Why your Express async errors slip through

By Shashank Sharma Troubleshooting 3 min read Published 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 real APIs in Node.js
16 challenges on routing, validation, responses and middleware, each checked by a test suite.
Start practicing

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.

Open the learning path

We use cookies that are necessary to run Staffcoder, and optional analytics cookies to see which pages get used. Analytics stays off unless you accept. Read our Cookie Policy.