Error Middleware

Centralized Error-Handling Middleware

An error handler is a middleware with four parameters. Express 24,430 checks fn.length === 4 to tell it apart from an ordinary layer, so the unused next must stay in the signature. Register it last, below the not-found layer of Mounting and 404s; everything above can then throw, and in Express 5 a rejecting async handler arrives here with no wrapper (Async Middleware).

The handler at the bottom of the stackJavaScript
app.use((req, res, next) => next(new AppError(`No route for ${req.method} ${req.originalUrl}`,
  { status: 404, code: 'route_not_found' })));
app.use((err, req, res, next) => {
  const status = Number.isInteger(err.status) && err.status >= 400 ? err.status : 500;
  if (status >= 500) logger.error({ err, method: req.method, url: req.originalUrl });
  if (res.headersSent) return next(err);          // hand back to finalhandler
  const message = status >= 500 ? 'Internal Server Error' : err.message;
  res.status(status).json({
    error: { code: err.code ?? 'internal_error', message,
             ...(err.details ? { details: err.details } : {}) },
    requestId: req.id
  });
});

Three GET responses from the running Bookshelf server, with the trailing requestId field trimmed for width, followed by the log line the third one writes to stderr:

Output of 50
404 /api/books/99      {"error":{"code":"not_found","message":"Book 99 does not exist"}}
404 /api/authors       {"error":{"code":"route_not_found",
                        "message":"No route for GET /api/authors"}}
500 /api/reports/sales {"error":{"code":"internal_error","message":"Internal Server Error"}}
{"level":"error","msg":"connect ECONNREFUSED 127.0.0.1:27017","name":"Error","status":500,
 "method":"GET","url":"/api/reports/sales","stack":"at .../server.js:45:37"}

The 500 is the one that matters. Its handler awaited a rejected promise carrying a connection failure; the client learns nothing, the detail goes to the log. Never derive a client message from err.message at 500 and above: internal messages quote table names, file paths and connection strings, and an attacker reads them as a map.

Omitting this middleware is worse than it looks. Express's built-in handler answers with an HTML page, and with NODE_ENV unset that page was 1,885 bytes holding the full stack, absolute file paths and node_modules frames. NODE_ENV=production shrank it to 148 bytes reading Internal Server Error — one environment variable between a stranger and your directory layout.