Unhandled errors in an Express app can crash the process or leak sensitive details to clients. This lesson covers Express's dedicated error-handling middleware and how to catch errors safely inside async routes.
Error-Handling Middleware
Express recognizes error-handling middleware purely by function arity, a middleware function with exactly four parameters, (err, req, res, next), is treated as an error handler instead of regular middleware. Calling next(err) anywhere in the pipeline skips directly to the nearest error handler.
Error-handling middleware should be registered last, after all your regular routes and middleware, so it can catch anything that gets passed to next(err) from earlier in the pipeline.
app.get('/users/:id', (req, res, next) => {
const user = findUser(req.params.id);
if (!user) {
return next(new Error('User not found'));
}
res.json(user);
});
// Registered last, after all routes
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: err.message });
});
Calling next(new Error(...)) instead of throwing directly skips straight to the error handler, without needing a manual try/catch in every route.
Error Handler Signature
app.use((err, req, res, next) => {
// must have exactly 4 parameters to be recognized as an error handler
res.status(500).json({ error: err.message });
});
Error handlers have exactly four parameters; three-parameter functions are treated as regular middleware.
Register error-handling middleware after every other route and middleware in the file.
Call next(err) to forward an error, don't call res.send() and next(err) for the same request.
try/catch and call next(err), or use an async-wrapper helper
Validation failure
res.status(400).json({ error: '...' }), often without needing next()
Not found resource
res.status(404).json({ error: 'Not found' })
Unexpected 500 error
Log full details server-side, return a generic client message
Centralized formatting
One error-handling middleware at the end of the pipeline
Catching Errors in Async Routes
Express 4's automatic error catching only works for synchronous throws. If an async route handler rejects (throws inside an awaited call), that rejection becomes an unhandled promise rejection unless you explicitly catch it and call next(err).
A small wrapper function that automatically forwards rejected promises to next() removes the need to repeat this try/catch in every single async route.
Avoiding Leaked Error Details
Returning a raw error's message or stack trace to clients can leak internal implementation details, or worse, sensitive data. Log full details server-side, and send a generic, safe message to the client for unexpected 500-level errors.
app.use((err, req, res, next) => {
console.error(err); // full details, server-side only
const status = err.statusCode || 500;
const message = status === 500 ? 'Internal server error' : err.message;
res.status(status).json({ error: message });
});
Common Mistakes
Assuming Express automatically catches errors thrown inside async route handlers, it does not without explicit handling.
Writing an error handler with three parameters instead of four, so Express treats it as regular middleware.
Sending a response and also calling next(err) for the same request, causing "headers already sent" errors.
Returning raw stack traces or internal error messages directly to API clients.
Key Takeaways
Express identifies error-handling middleware by exactly four parameters: (err, req, res, next).
Register error-handling middleware last, so it can catch errors from any earlier route.
Async route handlers need explicit try/catch and next(err), Express doesn't catch async errors automatically.
Log full error details server-side, but return generic, safe messages to clients for unexpected failures.
Pro Tip
Write one small asyncHandler(fn) wrapper that catches rejected promises and calls next(err) automatically, then wrap every async route with it. It's a tiny amount of code that eliminates a very common and easy-to-miss bug.
You now handle errors safely in Express. Next, learn how to serve static files properly using Express's built-in static middleware.