Why your Express async errors slip through

Troubleshooting 8 min read Updated August 2026

You wrote a clean error middleware, tested it with a synchronous throw, and it worked. Then an await in a route rejects — and instead of a tidy 500, the request hangs and your handler never fires. This is one of the most common Express bugs in production, and it comes down to a single gap in how Express 4 handles promises.

Why the error vanishes

Express 4's routing was written before async/await. It wraps each handler in a try/catch, so a synchronous throw is caught and forwarded to your error middleware. But an async function returns a promise, and Express 4 doesn't await it — so when that promise rejects, Express never sees the error:

// looks fine — but the rejection is invisible to Express 4
app.get("/users/:id", async (req, res) => {
  const user = await db.users.findById(req.params.id)   // throws → rejects
  res.json(user)
})
// no response is ever sent; the request hangs until it times out

The throw becomes an unhandled promise rejection. Your app.use((err, req, res, next) => …) handler is never called because no one passed the error to next.

Fix 1: pass it to next() yourself

The mechanical fix is a try/catch in every handler that forwards the error:

app.get("/users/:id", async (req, res, next) => {
  try {
    const user = await db.users.findById(req.params.id)
    res.json(user)
  } catch (err) {
    next(err)          // now the error reaches your middleware
  }
})

Correct, but repetitive — and the day someone forgets the try/catch, the bug is back. It doesn't scale past a couple of routes.

Fix 2: a wrapper (the standard pattern)

Write the try/catch once as a higher-order function, then wrap every async handler with it:

// catch any rejection and forward it — one wrapper, every route
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 db.users.findById(req.params.id)
  res.json(user)          // any rejection now lands in your error middleware
}))

This is what libraries like express-async-handler do. One small function removes every try/catch and makes it impossible to forget — the rejection always flows to next.

Practice this
Build robust Express APIs
Practice error handling, async routes, and middleware in Node challenges — graded automatically.
Practice backend

Fix 3: upgrade to Express 5

Express 5 closes the gap: it awaits the promise returned by async middleware and forwards a rejection to your error handler automatically. On Express 5, the naive version just works:

// Express 5 — a plain throw in an async route reaches error middleware
app.get("/users/:id", async (req, res) => {
  const user = await db.users.findById(req.params.id)
  if (!user) throw new AppError(404, "Not found")   // caught for you
  res.json(user)
})

If you're starting fresh, this is the cleanest option. On an existing Express 4 app, the asyncHandler wrapper is the low-risk fix.

Don't forget the safety net

Whichever fix you use, keep a process-level guard so a stray rejection can't silently take the server down:

process.on("unhandledRejection", (reason) => {
  logger.error({ reason }, "unhandled promise rejection")
  // let your process manager restart a truly broken process
})

The takeaway: on Express 4 an async rejection never reaches your error middleware on its own. Wrap your handlers (or move to Express 5), and every failure — sync or async — ends up in the one place that formats it.

FAQ

Why isn't my Express error handler catching errors?

Express 4 only forwards synchronous throws to error middleware. When an async handler rejects, Express doesn't see it — the promise rejection is unhandled and the request hangs. You have to pass the error to next() yourself, or wrap handlers so it happens automatically.

Does Express 5 fix async errors?

Largely, yes. Express 5 automatically forwards rejected promises from async middleware to your error handler, so a plain 'throw' inside an async route works. On Express 4 you still need a wrapper or manual next(err).

What is an asyncHandler wrapper?

A small higher-order function that wraps an async route, catches any rejection, and calls next(err) for you — so one wrapper replaces try/catch in every handler.

Put it into practice

Solve real challenges in a full browser IDE — graded automatically.

Browse frameworks