Unhandled errors still crash production apps in 2026. Most codebases have try/catch but no real error strategy. Here are 6 patterns you can copy today that actually prevent outages.
1. Replace console.error with structured logging
The default pattern logs errors but gives you nothing to debug later.
Before
try {
const res = await fetch('/api/users')
const data = await res.json()
setUsers(data)
} catch (err) {
console.error(err)
}
After
import { logger } from '@/lib/logger'
try {
const res = await fetch('/api/users')
if (!res.ok) {
throw new Error(`Users API returned ${res.status}`)
}
const data = await res.json()
setUsers(data)
} catch (err) {
logger.error('Failed to fetch users', {
error: err instanceof Error ? err.message : String(err),
stack: err instanceof Error ? err.stack : undefined,
endpoint: '/api/users',
component: 'UsersList'
})
}
Now every error is queryable JSON instead of unreadable console output. This is the difference between debugging in seconds vs hours.
2. Introduce a real AppError class
Native Error is too weak for production. You need codes and metadata.
Before
throw new Error('User not found')
After
export class AppError extends Error {
constructor(
public message: string,
public code: string,
public statusCode: number = 500,
public context: Record<string, unknown> = {}
) {
super(message)
Error.captureStackTrace(this, this.constructor)
}
}
throw new AppError(
'User not found',
'USER_NOT_FOUND',
404,
{ userId }
)
Now your backend and frontend can handle errors predictably instead of string matching.
3. Centralize API error handling in Express
Without middleware, every route handles errors differently.
Before
app.get('/users/:id', async (req, res) => {
try {
const user = await getUser(req.params.id)
res.json(user)
} catch (err) {
res.status(500).json({ message: 'Error' })
}
})
After
app.get('/users/:id', async (req, res, next) => {
try {
const user = await getUser(req.params.id)
res.json(user)
} catch (err) {
next(err)
}
})
export function errorHandler(err, req, res, next) {
if (err instanceof AppError) {
return res.status(err.statusCode).json({
error: {
code: err.code,
message: err.message
}
})
}
return res.status(500).json({
error: {
code: 'INTERNAL_ERROR',
message: 'Unexpected error'
}
})
}
You remove duplicated logic and standardize every response. This cuts frontend error handling complexity by half.
4. Use React Error Boundaries at multiple levels
One crash should not kill your entire UI.
Before
export default function Dashboard() {
return (
<>
<RevenueChart />
<RecentOrders />
</>
)
}
After
<ErrorBoundary level="section">
<RevenueChart />
</ErrorBoundary>
<ErrorBoundary level="section">
<RecentOrders />
</ErrorBoundary>
class ErrorBoundary extends React.Component {
state = { hasError: false }
static getDerivedStateFromError() {
return { hasError: true }
}
render() {
if (this.state.hasError) {
return <div>Section failed to load</div>
}
return this.props.children
}
}
Now one broken component does not take down the entire page. This is critical for dashboards and complex UIs.
5. Retry unstable APIs with exponential backoff
External APIs fail. Your app should not.
Before
const result = await paymentProvider.charge(amount)
After
async function withRetry(fn, maxAttempts = 3) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn()
} catch (err) {
if (attempt === maxAttempts) throw err
const delay = 1000 * Math.pow(2, attempt - 1)
await new Promise(r => setTimeout(r, delay))
}
}
}
const result = await withRetry(() =>
paymentProvider.charge(amount)
)
This prevents transient failures from becoming user-visible errors. In real systems this alone can reduce failure rates by 30 to 50 percent.
6. Use Promise.allSettled for graceful degradation
One failed request should not blank your UI.
Before
const [revenue, orders] = await Promise.all([
fetchRevenue(),
fetchOrders()
])
After
const [revenue, orders] = await Promise.allSettled([
fetchRevenue(),
fetchOrders()
])
return (
<>
{revenue.status === 'fulfilled' ? (
<RevenueChart data={revenue.value} />
) : (
<div>Revenue unavailable</div>
)}
{orders.status === 'fulfilled' ? (
<OrdersList data={orders.value} />
) : (
<div>Orders unavailable</div>
)}
</>
)
Now partial failures do not destroy the entire experience. This is especially important for dashboards and multi-source pages.
This pattern also pairs well with deeper debugging workflows like the ones in this systematic JavaScript debugging guide when something does go wrong.
Closing
Pick one of these patterns and add it to your codebase today. Start with structured logging or API error middleware. Then layer in retries and boundaries.
Most production outages are not complex bugs. They are missing error handling. Fix that once, and your entire system becomes more stable overnight.
Top comments (0)