Error Handling in Express
Centralize error responses with four-argument error middleware and let Express 5 handle async rejections automatically.
Search across all documentation pages
Centralize error responses with four-argument error middleware and let Express 5 handle async rejections automatically.
Quick-reference recipe card - copy-paste ready.
import express, { type Request, type Response, type NextFunction } from "express";
class AppError extends Error {
constructor(
public statusCode: number,
message: string,
public code?: string
) {
super(message);
}
}
const app = express();
app.get("/users/:id", async (req, res) => {
const user = await findUser(req.params.id);
if (!user) throw new AppError(404, "User not found", "USER_NOT_FOUND");
res.json(user);
});
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
const status = err instanceof AppError ? err.statusCode : 500;
const code = err instanceof AppError ? err.code : "INTERNAL_ERROR";
res.status(status).json({ error: err.message, code });
});When to reach for this: Any API that returns consistent error shapes instead of HTML stack traces or hung requests.
import express, { type Request, type Response, type NextFunction } from "express";
import { z } from "zod";
class AppError extends Error {
constructor(public statusCode: number, message: string) {
super(message);
}
}
const createUserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
});
const app = express();
app.use(express.json());
function validateBody<T>(schema: z.ZodSchema<T>) {
return (req: Request, _res: Response, next: NextFunction) => {
const result = schema.safeParse(req.body);
if (!result.success) {
next(new AppError(422, result.error.message));
return;
}
req.body = result.data;
next();
};
}
app.post("/users", validateBody(createUserSchema), async (req, res) => {
const user = await createUser(req.body);
res.status(201).json(user);
});
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
console.error(err);
const status = err instanceof AppError ? err.statusCode : 500;
res.status(status).json({
error: status === 500 ? "Internal server error" : err.message,
});
});
async function createUser(data: z.infer<typeof createUserSchema>) {
return { id: 1, ...data };
}What this demonstrates:
AppError with status codesnext(err) instead of handling inlineasync handlers automaticallynext(err) skips remaining middleware and jumps to the first 4-arg handler| Status | Meaning | Example |
|---|---|---|
| 400 | Malformed request | Invalid JSON |
| 401 | Not authenticated | Missing token |
| 403 | Not authorized | Valid token, wrong role |
| 404 | Not found | Unknown resource ID |
| 422 | Validation failed | Zod schema rejection |
| 500 | Server error | Unhandled exception |
// Typed error handler
const errorHandler: express.ErrorRequestHandler = (err, _req, res, _next) => {
res.status(500).json({ error: "Internal server error" });
};
app.use(errorHandler);res.json() then next(err) - double response crash. Fix: return after sending, never call next after respond.next(err) in error middleware - passes to the next error handler; calling next() without err skips to regular middleware. Fix: end the response in the final error handler.err.stack in production - information leak. Fix: log stack server-side, send generic message to client.| Alternative | Use When | Don't Use When |
|---|---|---|
express-async-errors (Express 4) | Stuck on Express 4 | Already on Express 5 |
Fastify setErrorHandler | Fastify app with schema errors | Express codebase |
| NestJS exception filters | Decorator-based HTTP exceptions | Simple Express API |
| Result type (neverthrow) | Functional error handling in services | Team prefers throw/catch |
No. Express 5 natively forwards async errors to error middleware.
Operational errors (404, validation) get specific messages. Programmer errors (TypeError) log fully and return generic 500.
Both are common. Pick one and document it in your API standards. 422 is popular for semantic validation failures.
Supertest: request(app).get("/missing").expect(404) and verify JSON error shape.
Register process.on("unhandledRejection") to log and gracefully shut down. Do not rely on Express to catch those.
Yes. Call next(err) in the first to pass to the next. The last handler should always send a response.
Document error response schemas per status code. Error middleware should match those documented shapes.
Prefer res.status(500).json({...}) for APIs. sendStatus returns plain text.
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.
Reviewed by Chris St. John·Last updated Jul 18, 2026