Diagnosing and Fixing Unhandled Promise Rejections in Express Routes
When an async Express route throws an error, you may see a 500 response, a hanging request, or even a process crash. This guide walks through the symptoms, root causes, step‑by‑step checks, and proven fixes for Express 4.x and the upcoming Express 5.x behavior.
20 Mar 2026, 11:20 UTC

Problem
When an async route handler throws or returns a rejected promise, you may see one of the following:
- Client request times out or receives no response.
- Server responds with a generic 500 status but no stack trace.
- Node process exits immediately due to an unhandled rejection.
These symptoms indicate that Express did not capture the rejection and forward it to the error‑handling middleware.
Recognizable Condition
Send a request to an async route that intentionally throws an error and observe the server’s behavior. If the request hangs or the process dies, you’re in the diagnostic loop.
Common Causes
| Cause | Express 4.x | Express 5.x |
|---|---|---|
| Async handler returns a rejected promise | Not caught; propagates to Node’s unhandledRejection | Automatically caught and forwarded to next(err) |
| Missing error‑handling middleware | Default 500 response or no response | Default 500 response |
| Error middleware registered before routes | Errors bypass middleware | Errors bypass middleware |
| Wrapper that discards this or context | Breaks route logic | Breaks route logic |
| Node configured to exit on unhandledRejection | Causes crash | No effect (Express handles it) |
| Using Express 5.x in production without alpha‑flags | N/A | Potential breaking changes |
Ordered Checks
- Verify Express version
Runnpm list expressto confirm you’re on 4.x or 5.x. - Test the route in isolation
Usecurl -i http://localhost:3000/testafter addingapp.get('/test', async (req, res) => { throw new Error('boom'); });. - Observe process behavior
Check if the request hangs, a 500 is sent, or the Node process exits. - Check for
unhandledRejectionlistener
Runnode -e "process.on('unhandledRejection', r=>console.log('unhandled', r)); require('./app');"to see if the rejection reaches Node. - Confirm error middleware order
Ensureapp.use((err, req, res, next) => { /* log & send */ });is added after all routes. - Check wrapper usage
If a helper likeasyncHandleris used, verify it preservesthisand arguments. - Validate Express 5.x alpha flag
When usingexpress@next, confirmprocess.env.EXPRESS_DEBUG=1is set if you want verbose logs.
Fixes Tied to Findings
1. Express 4.x – Wrap Async Handlers
Express 4.x does not automatically catch promise rejections. Wrap each async handler:
const asyncHandler = fn => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
app.get('/data', asyncHandler(async (req, res) => {
const data = await db.fetch();
res.json(data);
}));
Benefits:
- Single place to handle errors.
- Preserves
thisvia arrow function binding. - Works with any promise‑returning middleware.
2. Express 4.x – Add Global UnhandledRejection Listener
To avoid silent crashes, add a listener that logs and exits on unhandled rejections:
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
process.exit(1);
});
Use this only if you prefer Node to terminate rather than silently ignoring the error.
3. Express 5.x – No Wrapper Needed
Express 5.x automatically forwards rejected promises. Declare the route as async and add standard error middleware:
app.get('/data', async (req, res) => {
const data = await db.fetch();
res.json(data);
});
// Error middleware must come last
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ error: err.message });
});
Remember that Express 5.x is still alpha; enable it with npm install express@next and set process.env.EXPRESS_DEBUG=1 for verbose output.
4. Ensure Middleware Order
Express processes middleware in the order it’s defined. Place error middleware after all routes and other middleware:
app.use(express.json());
app.use('/api', apiRouter); // routes
app.use((err, req, res, next) => { /* error handling */ });
If placed before routes, errors will never reach it.
5. Preserve Context in Wrappers
A naive wrapper that ignores this can break handlers that reference this.app or similar. Use arrow functions or bind this explicitly:
const asyncHandler = fn => function (req, res, next) {
Promise.resolve(fn.call(this, req, res, next)).catch(next);
};
Escalation Criteria
- If the request still times out after applying the wrapper, check network latency or database timeouts.
- If the process continues to crash on every unhandled rejection, remove the
unhandledRejectionlistener and rely on Express’s built‑in handling (4.x wrapper or 5.x auto). - If you’re on Express 5.x alpha and encounter unexpected behavior, downgrade to 4.x until the stable release is available.
- When using third‑party middleware that returns promises, confirm it also forwards errors via
next()or use the global wrapper pattern.
Practical Verification
- Create a minimal app (app.js):
const express = require('express'); const app = express(); // Async route that throws app.get('/test', async (req, res) => { throw new Error('boom'); }); // Error middleware app.use((err, req, res, next) => { console.error('Error caught:', err.message); res.status(500).json({ error: err.message }); }); app.listen(3000, () => console.log('Server ready')); - Run
node app.jsand requesthttp://localhost:3000/test. In Express 4.x you’ll see a crash or timeout; in Express 5.x you’ll get a 500 JSON response. - Apply the
asyncHandlerwrapper and re‑run. The 500 response should now appear consistently in 4.x. - Add
process.on('unhandledRejection', …)and observe the console output when the route throws.
Limitations & Caveats
- Express 5.x is alpha; use it only in controlled environments.
- Global
unhandledRejectionlisteners may mask errors that Express would otherwise surface. - Wrapping every async handler increases boilerplate; consider a lint rule or a custom wrapper library for consistency.
- Ensure the wrapper does not alter the stack trace; use
Promise.resolveto preserve original error.
Takeaway
In Express 4.x, unhandled promise rejections in route handlers are silent bugs that can cause hangs or crashes. Wrap async handlers or upgrade to Express 5.x, which adds automatic promise handling. Always place error middleware after routes, preserve context in wrappers, and be cautious when using alpha releases. Following the ordered checks and fixes above will give you a reliable error‑handling path for both current and future Express versions.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.