Node.js › Error Handling

Why doesn't Express automatically catch errors thrown inside an async route handler in versions prior to Express 5, and how do you work around this?

Intermediate

Answer

In Express 4 and earlier, if an async function passed as a route handler throws (or its returned promise rejects), Express's underlying error-handling doesn't automatically catch it the way it does for synchronous throws, because Express predates widespread async/await and doesn't await the handler's returned promise -- the common workaround wraps every async handler in a helper that catches rejections and forwards them to next(), or uses a library like express-async-errors that patches this behavior globally.
// Manual wrapper approach
const asyncHandler = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);

app.get('/users/:id', asyncHandler(async (req, res) => {
  const user = await getUserOrThrow(req.params.id); // if this throws, asyncHandler catches and forwards it
  res.json(user);
}));
Real-world example A team on Express 4 noticed that an unhandled rejection inside an async route handler was crashing their entire server rather than being caught by their error middleware, until they wrapped every async route with a small asyncHandler utility that properly forwards rejected promises to Express's next() function.

Common follow-ups: How does Express 5 change this default behavior regarding async route handlers?;What's the risk of forgetting to wrap even a single async route handler in a codebase relying on this manual pattern?