Skip to content

Express Error Handling

Reliable error handling is what separates a fragile Express app from a production-ready one. This lesson brings together the patterns from earlier lessons into a complete strategy.

A Complete Error Handling Strategy

A solid strategy has three parts: catch errors close to where they happen (try/catch or an async wrapper), forward them consistently with next(err), and handle them once, centrally, at the bottom of the middleware stack.

This keeps individual routes simple, they only need to catch and forward, while all formatting, logging, and status-code decisions live in one place.

const asyncHandler = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);

app.get('/users/:id', asyncHandler(async (req, res) => {
  const user = await db.findUser(req.params.id);
  if (!user) {
    const err = new Error('User not found');
    err.status = 404;
    throw err;
  }
  res.json(user);
}));

app.use((err, req, res, next) => {
  const status = err.status || 500;
  if (status === 500) console.error(err.stack);
  res.status(status).json({ error: status === 500 ? 'Internal server error' : err.message });
});

Throwing inside asyncHandler's wrapped function is automatically caught and forwarded to the centralized error handler.

Attaching HTTP Status to Errors

const err = new Error('Not found');
err.status = 404;
next(err);

// or with a custom error class (see Custom Errors lesson)
throw new NotFoundError('User not found');
  • Attach a status property to errors so the centralized handler knows which HTTP code to use.
  • Default to 500 for anything without an explicit status, treating it as unexpected.
  • Log full stack traces server-side for 500-level errors; never expose them to clients.
  • For expected failures (validation, not found, unauthorized), throw with the correct status directly.

Error Handling Cheatsheet

How to classify and respond to common error scenarios.

Error Type Status Code Client Message
Validation failure 400 Specific field errors
Missing/invalid auth 401 'Unauthorized'
Insufficient permissions 403 'Forbidden'
Resource not found 404 'Resource not found'
Duplicate/conflict 409 'Resource already exists'
Unexpected/bug 500 'Internal server error' (generic)

Operational vs Programmer Errors

Operational errors are expected failure modes your code anticipates, invalid input, a missing record, a failed external API call. Programmer errors are bugs, a typo, calling a function with the wrong arguments, an undefined variable. Treat them differently: operational errors get a clean, specific client response; programmer errors should be logged loudly and generally shouldn't reveal details to the client.

Process-Level Safety Nets

Even with careful in-app error handling, listen for uncaughtException and unhandledRejection at the process level as a last resort, logging the failure and shutting down gracefully rather than continuing in an unknown state.

process.on('unhandledRejection', (reason) => {
  console.error('Unhandled Rejection:', reason);
  process.exit(1);
});

process.on('uncaughtException', (err) => {
  console.error('Uncaught Exception:', err);
  process.exit(1);
});

Common Mistakes

  • Letting async errors go unhandled instead of using try/catch or an async wrapper.
  • Exposing raw stack traces or database error messages directly to API clients.
  • Treating every error the same way instead of distinguishing expected failures from bugs.
  • Having multiple inconsistent error-response shapes scattered across the codebase.

Key Takeaways

  • Catch errors close to the source, forward them with next(err), handle them centrally.
  • Attach a status property to errors so the central handler can respond correctly.
  • Distinguish operational errors (expected) from programmer errors (bugs) in how you log and respond.
  • Add process-level safety nets for truly unexpected failures as a last line of defense.

Pro Tip

Log the full error object server-side (console.error(err) or a real logger) for every 500-level response, but only ever send the client a short, generic message, detailed internals should never leave your server.