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
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.
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.
// 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.
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