Async Error Handling

Express 5 Catches Async Throws — Express 4 Didn't

Async Error Handling

Express 4 silently swallowed async errors. Express 5 forwards them to the error middleware automatically.

3 min read Level 2/5 #express#async#errors
What you'll learn
  • Know the Express 4 vs 5 difference
  • Use express-async-handler on 4
  • Recognize the silent-bug variant

The single biggest improvement in Express 5: async errors flow to your error middleware automatically.

Express 4 — the Trap

// Express 4
app.get("/users/:id", async (req, res) => {
  const user = await db.users.findById(req.params.id);
  if (!user) throw new Error("not found");   // ← silently lost
  res.json(user);
});

In Express 4, that throw becomes an unhandled promise rejection. Express’s error middleware never fires. The request hangs until the client times out. Awful.

Express 4 Fix — express-async-handler

npm install express-async-handler
import asyncH from "express-async-handler";

app.get("/users/:id", asyncH(async (req, res) => {
  const user = await db.users.findById(req.params.id);
  if (!user) throw new Error("not found");
  res.json(user);
}));

asyncH wraps each handler in a .catch(next). Errors flow correctly.

Or roll your own:

const asyncH = (fn) => (req, res, next) =>
  Promise.resolve(fn(req, res, next)).catch(next);

Express 5 — No Wrapper Needed

app.get("/users/:id", async (req, res) => {
  const user = await db.users.findById(req.params.id);
  if (!user) throw new HttpError(404, "not_found", "user not found");
  res.json(user);
});

Throw, reject, return a promise that rejects — Express 5 catches all of them and routes to your error middleware.

Middleware Too

The same applies to async middleware:

async function requireAuth(req, res, next) {
  req.user = await verifyToken(req.headers.authorization);
  next();
}

If verifyToken throws, the error flows to your error middleware.

The Subtle Bug

Don’t forget to await:

// missing await — error from db.users.create is silently lost
app.post("/users", async (req, res) => {
  db.users.create(req.body);   // 🚨
  res.status(201).end();
});

ESLint’s @typescript-eslint/no-floating-promises rule (TS) or promise/always-return (JS) catches these.

Why It Matters

In production, this difference is huge. Express 4 apps that don’t wrap async handlers leak request handles, hang clients, and miss errors in monitoring. Upgrade to Express 5 if you possibly can.

Middleware Order →