Centralizing Express Errors with Middleware: A Practical Guide
Centralize Express errors with a single four‑parameter middleware, use AppError and router‑scoped handlers, verify async behavior, and watch for hidden context.
19 Apr 2026, 05:30 UTC

Why error handling feels scattered in Express apps
When a route throws an error or calls next(err), you typically write res.status(500).json({...}) in every handler. This repeats status‑code logic and makes the happy‑path code noisy, forcing developers to duplicate response‑shaping and logging code.
Thesis: a single, centralized error‑handling middleware
By declaring one middleware that matches the four‑parameter signature (err, req, res, next), Express automatically routes any error that reaches it. Controllers stay focused on the successful flow, while the middleware normalizes responses, logs details, and returns safe messages to clients.
How Express matches error handlers
- Any
next(err)call creates an error object. - The first middleware with four parameters after that point receives the error.
- Handlers must declare exactly four arguments; adding a fifth or omitting
nextmakes Express treat it as regular middleware and it will never run.
Understanding this contract is the foundation for reliable centralization.
Simple AppError and JSON error handler
class AppError extends Error {
constructor(statusCode, message, code = 'ERR') {
super(message);
this.statusCode = statusCode;
this.code = code;
Error.captureStackTrace(this, this.constructor);
}
}
app.use((err, req, res, next) => {
// Log full error server‑side
console.error(err);
// Build safe response
const status = err.statusCode || 500;
res.status(status).json({
error: {
message: err.message,
code: err.code
}
});
});
Place this handler after all routes. It catches errors from any matched path and returns a consistent JSON structure.
Router‑scoped handling and 404 middleware
Mount a router under /api and attach its own error handler there. Anything that bubbles out of /api continues to the app‑level handler, allowing you to separate API‑specific and generic logic.
- Define a 404 handler as regular middleware after routes but before the error handler:
- Because it runs after routes, it only triggers when no earlier route matches.
app.use((req, res, next) => {
const err = new AppError(404, 'Not found');
next(err);
});
Async rejection caveat
In Express 4, a rejected promise from an async route does not automatically call next(err). You must either wrap the handler in try/catch or use a helper that forwards rejections.
app.get('/async', async (req, res, next) => {
try {
await Promise.reject(new AppError(400, 'Bad request'));
} catch (e) {
next(e); // forward to error handler
}
});
Without the try/catch, the error is swallowed and the JSON handler never runs.
Verification steps
- Check your Express version (e.g.,
npm list express) to know whether you need thetry/catchwrapper. - Add a temporary route
app.get('/boom', (req, res, next) => next(new Error('boom')))and runcurl -i http://localhost:3000/boom. Verify the JSON payload and correct status code appear. - Start the app with
NODE_ENV=productionand repeat the request; ensure no stack traces leak to the client.
These checks confirm that your error‑handling setup behaves as expected for the installed framework version.
Trade‑off: hidden context
Centralizing errors can obscure the exact source of a failure. Attach contextual data such as a request ID or the route name inside the error object before logging, so you can trace back to the originating code path.
Actionable closing
- Create an
AppErrorclass to carry status codes and error codes. - Write a single error‑handling middleware that logs the full error and returns a safe JSON response.
- Register router‑scoped handlers and a 404 middleware before the global error handler.
- Verify async routes with a test request and confirm behavior against your Express version.
Following these steps gives you a maintainable, version‑aware error‑handling strategy for any Express application.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.