Express 4.x error handlers require four arguments and manual async forwarding
Express only invokes error middleware with the (err, req, res, next) signature, and async rejections must be forwarded with next(err).
14 Jun 2026, 19:28 UTC

The problem: your error handler never runs
You add a route that calls next(new Error('database unavailable')), but users still receive Express's default HTML error page, or the request hangs until it times out. The cause is usually not the error itself. Express ignores ordinary middleware during an error flow unless that middleware declares the exact four-argument error signature, and asynchronous rejections never reach it unless you forward them manually.
This post explains how Express 4.x selects error handlers, where to mount them, and how to handle async route errors without losing the request.
Express identifies error handlers by argument count
Express inspects a middleware function's length—the number of declared parameters—to decide whether it is an error handler. A function declared as (err, req, res, next) is treated as error-handling middleware. A function declared as (req, res, next) is skipped when an error passes through next(err).
You can use an arrow function, but it must still declare four parameters. Do not add extra arguments just to match a pattern; Express uses arity, not the parameter names.
Mount error handlers after your routes
Middleware order matters. Mount your error handler after all application routes and route middleware. If you place it before a route, Express will not consult that handler for errors thrown by later handlers.
You can mount multiple error handlers for different categories—for example, one for validation errors and another for unexpected server errors—but each must appear after the routes it is intended to catch.
Async errors require explicit forwarding
Express catches errors thrown synchronously inside a route handler. It does not automatically catch rejected promises or async function rejections. If an async handler throws and you do not call next(err), the request can hang with no response.
Wrap async logic in a try/catch block and pass the error to next, or use a third-party wrapper such as express-async-errors if you prefer automatic propagation.
Worked example: sync and async errors in one Express app
This example assumes Express 4.x. Check your installed version before copying the pattern.
const express = require('express');
const app = express();
app.get('/sync-boom', (req, res, next) => {
throw new Error('synchronous failure');
});
app.get('/async-boom', async (req, res, next) => {
try {
await Promise.reject(new Error('asynchronous failure'));
} catch (err) {
next(err);
}
});
app.use((req, res) => {
res.status(404).send('Not found');
});
app.use((err, req, res, next) => {
if (res.headersSent) {
return next(err);
}
const status = err.status || 500;
const body = {
message: err.message
};
if (process.env.NODE_ENV !== 'production') {
body.stack = err.stack;
}
res.status(status).json(body);
});
app.listen(3000, () => {
console.log('Listening on port 3000');
});
How to verify the behavior
- Open a terminal in your project directory. No special permissions are required.
- Confirm the Express major version:
npm ls express. The output should show express@4.x. - Start the application:
node server.js, replacingserver.jswith your entry file name. - In another terminal, request the sync route:
curl -i http://localhost:3000/sync-boom. - Request the async route:
curl -i http://localhost:3000/async-boom.
Expected checks: both responses should return status 500 and a JSON body containing message. With NODE_ENV=production, the body should omit stack. If you omit next(err) in the async route, the request will hang instead of reaching the error handler.
Risk: calling res.json after another handler has already started the response throws ERR_HTTP_HEADERS_SENT. The res.headersSent guard in the example prevents that by delegating to the next error handler when the response has already begun.
Limitation: production error details and Express 5
Never include err.stack or full error objects in production HTTP responses. Stack traces can leak file paths, module names, and other internal details. The example conditionally includes the stack only when NODE_ENV is not production.
Express 5 changes error-handling and async behavior. Verify your major version with npm ls express before adopting patterns written for Express 4 documentation.
Closing checklist
- Declare error middleware with exactly four parameters:
(err, req, res, next). - Mount it after all routes and route middleware.
- Forward async rejections with
next(err)or use an async wrapper. - Check
res.headersSentbefore sending a response body. - Exclude stack traces from production responses.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.