Handling Async Errors in Express Route Handlers
Learn why async route handlers can leave requests hanging and how a simple wrapper or try/catch plus proper error‑middleware placement keeps your API reliable.
18 Nov 2025, 01:57 UTC

The problem: async route handlers can leave requests hanging
When you define an Express route with an async function, any thrown error or rejected promise becomes an unhandled rejection. Express’s default error‑page middleware only catches synchronous errors, so the request never reaches your error‑handling middleware and the client waits until it times out.
Solution: wrap async handlers to catch rejections
A small higher‑order function converts any async route handler into a form that forwards errors to next. This guarantees that every rejection is treated as an Express error.
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
// usage
app.get('/users/:id', asyncHandler(async (req, res) => {
const user = await db.findUser(req.params.id);
if (!user) throw new Error('User not found');
res.json(user);
}));
The wrapper calls the handler, awaits its promise, and passes any caught error to next. You can also place a try/catch inside the handler, but the wrapper protects you from forgetting it in any route.
Error‑middleware placement matters
Express looks for middleware with four arguments ((err, req, res, next)) and skips it unless an error is passed via next(err). Mount this middleware after all regular routes; otherwise it will never see the error.
app.use((err, req, res, next) => {
// In production avoid leaking stack traces
const status = err.status || 500;
res.status(status).json({ error: err.message });
});
Trade‑off and limitation
- Flexibility vs. boilerplate: The wrapper adds a line per route but eliminates the need to remember
try/catcheverywhere. - Error‑middleware must call
next: If you omitnext(or call it without an argument) inside the error handler, the request ends without a response, causing client timeouts and possible resource leaks. - Version note: The behavior described holds for Express 4.x and 5.x with Node 14+. Some third‑party error‑middleware packages may swallow errors unless you explicitly forward them with
next(err).
Practical verification
- Start your Express app locally.
- Send a request that triggers an error, e.g.
curl -i http://localhost:3000/users/999where user 999 does not exist. - Check the response: you should receive a JSON body with an
errorfield and a status code of 500 (or the status you set). - Ensure the connection closes promptly; there should be no hanging request.
- Optionally, inspect server logs to confirm no “UnhandledPromiseRejectionWarning” appears.
If you see a timeout or a blank response, verify that your error‑middleware has four parameters and is placed after all app.use and route definitions.
Closing
Async route handling in Express is straightforward once you guarantee every rejection reaches the error‑middleware chain. A tiny wrapper or disciplined try/catch plus correctly positioned error middleware prevents hanging requests and keeps your API predictable.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.