Production error handling for Express APIs — error middleware, async wrappers,
89
86%
Does it follow best practices?
Impact
97%
2.02xAverage score across 4 eval scenarios
Passed
No known issues
Production-grade error handling that belongs in every Express application from day one. When building any Express API -- whether a quick prototype or a production service -- always include these patterns without being asked.
Error handling is not a "harden later" task. Without it from the start:
SQLITE_CONSTRAINT: UNIQUE constraint failed), or HTML error pages instead of JSON.{ error: "..." }, another returns { message: "..." }, a third returns a bare string. Mobile clients cannot parse errors reliably.SIGTERM from Kubernetes kills in-flight requests instantly.These are not edge cases. They are the first things that break in production.
Define a base error class and common subtypes so routes throw typed errors with status codes:
// src/errors.ts
export class AppError extends Error {
constructor(
public statusCode: number,
message: string,
public code?: string
) {
super(message);
this.name = 'AppError';
}
}
export class NotFoundError extends AppError {
constructor(resource: string, id?: string) {
super(404, id ? `${resource} ${id} not found` : `${resource} not found`, 'NOT_FOUND');
}
}
export class ValidationError extends AppError {
constructor(message: string, public details?: { field: string; message: string }[]) {
super(400, message, 'VALIDATION_ERROR');
}
}Express 4 does not forward rejected promises to error middleware. Wrap every async route:
// src/middleware/async-handler.ts
import { Request, Response, NextFunction, RequestHandler } from 'express';
export function asyncHandler(
fn: (req: Request, res: Response, next: NextFunction) => Promise<any>
): RequestHandler {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}Use it on every async route:
router.get('/api/books/:id', asyncHandler(async (req, res) => {
const book = await getBook(req.params.id);
if (!book) throw new NotFoundError('Book', req.params.id);
res.json({ data: book });
}));A single error handler with 4 parameters that all errors flow through:
// src/middleware/error-handler.ts
import { Request, Response, NextFunction } from 'express';
import { AppError } from '../errors';
export function errorHandler(err: Error, req: Request, res: Response, _next: NextFunction) {
console.error(`[${req.method} ${req.path}]`, err.message);
if (!(err instanceof AppError)) {
console.error(err.stack);
}
if (err instanceof AppError) {
const body: any = { error: { code: err.code, message: err.message } };
if ('details' in err && (err as any).details) {
body.error.details = (err as any).details;
}
res.status(err.statusCode).json(body);
return;
}
// Unknown error -- never leak internals
res.status(500).json({
error: {
code: 'INTERNAL_ERROR',
message: 'An unexpected error occurred',
}
});
}Register it after all routes:
app.use('/api', routes);
// 404 catch-all for unknown API routes (before error middleware)
app.use('/api', (_req, res) => {
res.status(404).json({
error: { code: 'NOT_FOUND', message: 'Endpoint not found' }
});
});
app.use(errorHandler); // Must be lastconst server = app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
function shutdown(signal: string) {
console.log(`${signal} received. Shutting down gracefully...`);
server.close(() => {
console.log('HTTP server closed');
process.exit(0);
});
setTimeout(() => {
console.error('Forced shutdown after timeout');
process.exit(1);
}, 10_000);
}
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));All errors must follow this shape:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request body"
}
}For validation errors with field-level detail:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request body",
"details": [
{ "field": "email", "message": "must be a valid email address" },
{ "field": "name", "message": "is required" }
]
}
}router.get('/api/weather/:city', asyncHandler(async (req, res) => {
try {
const data = await fetchWeather(req.params.city);
res.json({ data });
} catch (err: any) {
if (err.message === 'ETIMEDOUT') {
throw new AppError(504, 'Weather service timed out', 'UPSTREAM_TIMEOUT');
}
if (err.status === 503) {
throw new AppError(502, 'Weather service is unavailable', 'UPSTREAM_UNAVAILABLE');
}
throw err;
}
}));router.post('/api/users', asyncHandler(async (req, res) => {
try {
const user = await createUser(req.body);
res.status(201).json({ data: user });
} catch (err: any) {
if (err.code === 'SQLITE_CONSTRAINT_UNIQUE') {
throw new AppError(409, 'Email already exists', 'DUPLICATE');
}
throw err;
}
}));router.post('/api/orders', asyncHandler(async (req, res) => {
const errors: { field: string; message: string }[] = [];
const { customer_name, items } = req.body;
if (!customer_name?.trim()) {
errors.push({ field: 'customer_name', message: 'is required' });
}
if (!Array.isArray(items) || items.length === 0) {
errors.push({ field: 'items', message: 'must be a non-empty array' });
}
if (errors.length > 0) {
throw new ValidationError('Invalid request body', errors);
}
const order = await createOrder({ customer_name: customer_name.trim(), items });
res.status(201).json({ data: order });
}));Every Express app must have from the start:
{ error: { code, message } }