DEV Community

JSGuruJobs
JSGuruJobs

Posted on

6 JavaScript Error Handling Patterns That Prevent Production Outages

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)
}
Enter fullscreen mode Exit fullscreen mode

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'
  })
}
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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 }
)
Enter fullscreen mode Exit fullscreen mode

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' })
  }
})
Enter fullscreen mode Exit fullscreen mode

After

app.get('/users/:id', async (req, res, next) => {
  try {
    const user = await getUser(req.params.id)
    res.json(user)
  } catch (err) {
    next(err)
  }
})
Enter fullscreen mode Exit fullscreen mode
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'
    }
  })
}
Enter fullscreen mode Exit fullscreen mode

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 />
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

After

<ErrorBoundary level="section">
  <RevenueChart />
</ErrorBoundary>

<ErrorBoundary level="section">
  <RecentOrders />
</ErrorBoundary>
Enter fullscreen mode Exit fullscreen mode
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
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
)
Enter fullscreen mode Exit fullscreen mode

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()
])
Enter fullscreen mode Exit fullscreen mode

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>
    )}
  </>
)
Enter fullscreen mode Exit fullscreen mode

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)