Managing Express Error Middleware: Arity and Order
Express identifies error handlers by function arity (4 arguments) and executes them in registration order. Learn how to structure your middleware stack and handle async errors.
27 Mar 2026, 22:04 UTC

An Express application that returns a generic 500 response without logs, or crashes with a "headers already sent" error, is often not a bug in the business logic. Instead, it is usually a mismatch between how Express identifies error-handling middleware and where that middleware is placed in the application stack.
The core takeaway: Express identifies error handlers by their arity (the number of arguments a function accepts). A function with exactly four parameters is treated as an error handler. Because Express executes middleware in a first-in, first-out sequence, these handlers must be mounted after all routes and regular middleware to capture errors effectively.
The Arity Contract
Express does not use a special method or configuration to define an error handler; it inspects the function signature. A regular middleware function takes three arguments: (req, res, next). An error-handling middleware function must take four: (err, req, res, next).
If you omit the err parameter, Express treats the function as standard middleware. In this case, it will be skipped when next(err) is called, leading to silent failures where errors fall through to the default Express HTML error page instead of your custom logic.
Middleware Ordering and Execution
Middleware execution follows the order of registration via app.use() or router.use(). When a route handler calls next(err) or throws a synchronous error, Express skips all remaining non-error-handling middleware in the stack until it finds a function with an arity of four.
If you mount your error handler at the top of your file, it will never see errors generated by the routes defined below it. The error handler must be the final piece of the pipeline.
Worked Example: Centralized Error Handling
This example demonstrates a version-aware setup for Express 4.x, including a wrapper to handle the limitation where async route handlers do not automatically forward rejected promises.
const express = require('express');
const app = express();
// Wrapper to forward async errors to next()
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
app.use(express.json());
// Route 1: Synchronous error
app.get('/sync-error', (req, res, next) => {
throw new Error('Something went wrong synchronously');
});
// Route 2: Async error using the wrapper
app.get('/async-error', asyncHandler(async (req, res) => {
throw new Error('Something went wrong asynchronously');
}));
// Error handler MUST be defined last
app.use((err, req, res, next) => {
// Guard against headers already sent to prevent app crash
if (res.headersSent) {
return next(err);
}
const status = err.status || 500;
res.status(status).json({
success: false,
message: err.message
});
});
app.listen(3000, () => console.log('Server running on port 3000'));
Execution: Run this using node app.js. A request to /sync-error or /async-error will trigger the four-argument handler, returning a JSON response instead of an HTML stack trace.
Limitations and Version Differences
The most significant limitation in Express 4 is the lack of automatic promise rejection handling. If you use an async function as a route handler without a wrapper or a try/catch block calling next(err), the error will result in an unhandled promise rejection, and the error middleware will never be triggered.
Express 5 changes this behavior by automatically forwarding rejected promises to the error handler. When upgrading or starting a new project, verify your package.json version to determine if asyncHandler wrappers are necessary.
Practical Verification
- Check Arity: If a handler isn't firing, check
handler.length. It must be 4. - Verify Order: Add
console.logstatements to your regular middleware and your error handler. Ensure the error handler logs only after a failure occurs and never during a successful request. - Test Async: Create an
asyncroute that throws an error without a wrapper; observe that the error handler is ignored in Express 4.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.