In Express 4 24,430 a rejected promise inside an async handler went nowhere: the framework called your function, ignored the promise it returned, and the request hung until the client gave up. That is why so much existing code wraps every handler in an asyncHandler helper or installs express-async-errors. Express 5 fixed it at the source. The router inspects the value a layer returns and, if it is a promise, attaches a rejection callback that calls next(error) for you, so async handlers reach your error middleware with no wrapper.
app.get('/reject', async (req, res) => {
const book = await Promise.reject(new Error('database unreachable'));
res.json(book);
});
app.use((err, req, res, next) => res.status(503).json({ error: err.message }));GET /reject -> 503 {"error":"database unreachable"}Delete the wrappers when you migrate, but know the boundary: Express only sees the promise your function returns. Anything escaping it is still an uncaught exception that can take the process down:
a throw inside a setTimeout or process.nextTick callback, or inside a listener such as req.on('data', ...): wrap the callback body in try/catch and hand the error to next yourself;
an async call made without await or .catch() — the classic missing await, caught statically by the no-floating-promises lint rule;
a throw after the response was sent: guard with if (res.headersSent) return next(err); so Express closes the connection instead of adding ERR_HTTP_HEADERS_SENT on top of the original error.