Async Error Handling in Express 5

Express 5 finally auto-catches rejected promises from async handlers, retiring the try/catch wrapper you carried around in Express 4.

This is one of the headline reasons to move to Express 5. In Express 4, a rejected promise inside an async handler was not forwarded to your error handler — the request would hang until it timed out, and you leaned on the express-async-errors patch or a hand-rolled asyncHandler wrapper on every route.

Express 5 does the wrapping for you

The router now inspects the return value of your handler. If it returns a promise that rejects (or an async function that throws), Express calls next(err) automatically. You can simply throw.

// Express 5 — this reaches your error handler with no wrapper.
app.get('/user/:id', async (req, res) => {
  const user = await db.users.find(req.params.id);
  if (!user) throw new AppError('User not found', 404);
  res.json(user);
});

The one gotcha

Auto-catch only applies to the handler's own returned promise. If you kick off async work inside a callback (an event listener, a setTimeout, a stream 'data' handler) that rejects, Express cannot see it — you still try/catch and call next(err) there yourself.

Example

Example · javascript
import express from 'express';

const app = express();
app.use(express.json());

class AppError extends Error {
  constructor(message, status = 500) {
    super(message);
    this.status = status;
  }
}

// No try/catch, no wrapper — Express 5 forwards the rejection for you.
app.get('/orders/:id', async (req, res) => {
  const order = await fakeDb.findOrder(req.params.id); // may reject
  if (!order) throw new AppError('Order not found', 404);
  res.json(order);
});

// COUNTEREXAMPLE: work started inside a callback escapes auto-catch.
app.get('/broken', async (req, res, next) => {
  setTimeout(async () => {
    try {
      await fakeDb.mightReject();
      res.json({ ok: true });
    } catch (err) {
      next(err); // you must forward this one yourself
    }
  }, 10);
});

app.use((err, req, res, next) => {
  res.status(err.status ?? 500).json({ error: err.message });
});

const fakeDb = {
  findOrder: async (id) => (id === '1' ? { id, total: 42 } : null),
  mightReject: async () => { throw new Error('db down'); },
};

When to use it

  • A team standardises on a catchAsync wrapper so all async controllers automatically forward rejected promises to the Express error middleware.
  • A developer uses Express 5 which natively forwards async throws to next(err), removing the need for every handler to have a try/catch block.
  • A shared error library provides a typed AppError class and a catchAsync wrapper so every microservice handles async errors consistently.

More examples

catchAsync higher-order function

catchAsync wraps an async function and attaches .catch(next) so any rejection is forwarded to the error middleware.

Example · js
const catchAsync = fn => (req, res, next) =>
  fn(req, res, next).catch(next);

const getUser = catchAsync(async (req, res) => {
  const user = await UserService.findById(req.params.id);
  if (!user) throw new AppError('User not found', 404);
  res.json(user);
});

app.get('/users/:id', getUser);

Async error in Express 5

Shows Express 5 automatic async error handling where thrown errors inside async handlers reach the error middleware directly.

Example · js
// Express 5: no wrapper needed — thrown errors go to next(err) automatically
app.get('/orders/:id', async (req, res) => {
  const order = await db.orders.findById(req.params.id);
  if (!order) throw Object.assign(new Error('Not found'), { status: 404 });
  res.json(order);
});

app.use((err, req, res, next) => {
  res.status(err.status || 500).json({ error: err.message });
});

Async middleware forwarding errors

An async middleware populates req.targetUser and forwards both 404 and unexpected errors to the error middleware.

Example · js
async function resolveUser(req, res, next) {
  try {
    req.targetUser = await db.users.findById(req.params.userId);
    if (!req.targetUser) return next(new AppError('User not found', 404));
    next();
  } catch (err) {
    next(err);
  }
}

app.get('/users/:userId/posts', resolveUser, async (req, res) => {
  const posts = await db.posts.findByUser(req.targetUser.id);
  res.json(posts);
});

Discussion

  • Be the first to comment on this lesson.