Skip to content

Express Error-Handling Middleware

Error-handling middleware is a special kind of middleware that Express recognizes by its four-argument signature. This lesson shows how to centralize error handling instead of repeating try/catch blocks everywhere.

What Makes Middleware an Error Handler?

Express identifies error-handling middleware purely by its arity, a function with exactly four parameters, (err, req, res, next), is treated as an error handler. Everything else is treated as regular middleware, even if you intended it to catch errors.

Error handlers are skipped during normal request handling. They only run when a previous middleware or route handler calls next(err), or when a synchronous error is thrown inside a route handler.

app.get('/users/:id', (req, res, next) => {
  const user = users.find((u) => u.id === Number(req.params.id));
  if (!user) {
    return next(new Error('User not found'));
  }
  res.json(user);
});

// error-handling middleware, registered LAST
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: err.message });
});

Calling next(err) skips every remaining regular middleware/route and jumps directly to the four-argument error handler.

Error Middleware Syntax

app.use((err, req, res, next) => {
  // must have exactly 4 parameters to be recognized as an error handler
  res.status(err.status || 500).json({ error: err.message });
});
  • Error-handling middleware must be registered after all other app.use()/routes.
  • You can define multiple error handlers; Express calls the next one if you call next(err) again.
  • Synchronous throws inside a normal route handler are caught automatically by Express.
  • Errors inside async functions are NOT caught automatically in Express 4, you must catch and forward them yourself.

Error Handling Cheatsheet

How different kinds of errors reach your centralized handler.

Situation How the Error Reaches the Handler
Synchronous throw Caught automatically by Express, forwarded to error handler
Manual forward next(err) inside a route or middleware
Async/await error Must catch and call next(err) yourself (Express 4)
Validation failure next(new ValidationError(...)) from your validation layer
Unmatched route Custom 404 middleware, not an error handler, registered before error handlers

Handling Errors in Async Route Handlers

In Express 4, a rejected promise inside an async route handler does NOT automatically reach your error middleware, you must wrap it in a try/catch and call next(err) explicitly, or use a small wrapper helper.

// manual try/catch
app.get('/users/:id', async (req, res, next) => {
  try {
    const user = await db.findUser(req.params.id);
    if (!user) return res.status(404).json({ error: 'Not found' });
    res.json(user);
  } catch (err) {
    next(err);
  }
});

// reusable wrapper
const asyncHandler = (fn) => (req, res, next) => fn(req, res, next).catch(next);

app.get('/users/:id', asyncHandler(async (req, res) => {
  const user = await db.findUser(req.params.id);
  res.json(user);
}));

Express 5 automatically forwards rejected promises from async handlers to next(err), but explicit handling is still clear and works on both versions.

Building a Centralized Error Handler

A single, well-designed error handler at the bottom of the middleware stack keeps error formatting consistent across your entire API, instead of scattering res.status(500).json(...) calls throughout your codebase.

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

  if (status === 500) {
    console.error(err.stack);
  }

  res.status(status).json({ success: false, error: message });
});

Common Mistakes

  • Writing an error handler with three arguments instead of four, Express treats it as regular middleware and skips it for errors.
  • Registering the error handler before your routes, it will never be reached.
  • Forgetting to catch rejected promises inside async route handlers in Express 4.
  • Leaking internal error details (stack traces, SQL errors) directly to API clients in production.

Key Takeaways

  • Express recognizes error-handling middleware purely by its four-parameter signature.
  • next(err) skips remaining regular middleware and jumps to the nearest error handler.
  • Async handlers need explicit try/catch (or a wrapper) to forward rejected promises in Express 4.
  • A single centralized error handler keeps error responses consistent across the whole API.

Pro Tip

Write one small asyncHandler wrapper once and reuse it on every async route, it is far less error-prone than remembering a manual try/catch in every single handler.