Express Cheatsheet
Error Handling
Use this Express reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
How Express Error Handling Works
Any middleware or route handler that calls next(err) (with a truthy argument) bypasses remaining regular middleware and jumps to the nearest error-handling middleware — one with exactly four parameters: (err, req, res, next).
Triggering Errors
// Synchronous throw — Express catches this in routes (NOT in async code) app.get('/sync', (req, res) => { throw new Error('Sync error'); // caught by Express 4 for sync handlers only }); // next(err) — works everywhere, always preferred app.get('/data', async (req, res, next) => { try { const data = await db.query(); res.json(data); } catch (err) { next(err); // forward to error handler } }); // Pass an HTTP status with the error app.get('/secret', (req, res, next) => { const err = new Error('Forbidden'); err.status = 403; next(err); });
Express 4: Async errors from
asyncfunctions are NOT auto-caught — you must usetry/catch+next(err)or an async wrapper. Express 5: Rejected promises are forwarded tonext(err)automatically.
Error-Handling Middleware
Must be registered after all routes and other middleware. Must have exactly 4 parameters.
// Minimal error handler app.use((err, req, res, next) => { console.error(err.stack); res.status(err.status || 500).json({ error: { message: err.message, ...(process.env.NODE_ENV !== 'production' && { stack: err.stack }), }, }); });
Custom Error Classes
class AppError extends Error { constructor(message, status = 500, code = 'INTERNAL_ERROR') { super(message); this.name = 'AppError'; this.status = status; this.code = code; } } class NotFoundError extends AppError { constructor(resource = 'Resource') { super(`${resource} not found`, 404, 'NOT_FOUND'); } } class ValidationError extends AppError { constructor(details) { super('Validation failed', 400, 'VALIDATION_ERROR'); this.details = details; } } class UnauthorizedError extends AppError { constructor(msg = 'Unauthorized') { super(msg, 401, 'UNAUTHORIZED'); } }
Using them in routes:
app.get('/users/:id', async (req, res, next) => { try { const user = await db.users.findById(req.params.id); if (!user) throw new NotFoundError('User'); res.json(user); } catch (err) { next(err); } });
Production Error Handler
app.use((err, req, res, next) => { // Log every error with context console.error({ message: err.message, status: err.status, stack: err.stack, url: req.originalUrl, method: req.method, ip: req.ip, }); // Operational errors: known, expected if (err instanceof AppError) { return res.status(err.status).json({ error: { code: err.code, message: err.message, ...(err.details && { details: err.details }), }, }); } // Programmer/unexpected errors: don't leak details res.status(500).json({ error: { code: 'INTERNAL_ERROR', message: 'Something went wrong' } }); });
Async Handler Wrapper (Express 4)
Eliminates repetitive try/catch blocks.
const asyncHandler = fn => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next); // Usage app.get('/users', asyncHandler(async (req, res) => { const users = await db.users.findAll(); res.json(users); })); // Alternative: wrap all routes in a router const asyncRouter = express.Router(); ['get', 'post', 'put', 'patch', 'delete'].forEach(method => { const original = asyncRouter[method].bind(asyncRouter); asyncRouter[method] = (path, ...handlers) => { original(path, ...handlers.map(h => h.length === 4 ? h : (req, res, next) => Promise.resolve(h(req, res, next)).catch(next) )); }; });
Chaining Multiple Error Handlers
Use multiple error-handling middleware for separation of concerns.
// 1. Handle Multer file upload errors app.use((err, req, res, next) => { if (err.code === 'LIMIT_FILE_SIZE') { return res.status(413).json({ error: 'File too large' }); } next(err); }); // 2. Handle database constraint errors (e.g., pg) app.use((err, req, res, next) => { if (err.code === '23505') { // unique_violation in PostgreSQL return res.status(409).json({ error: 'Resource already exists' }); } next(err); }); // 3. Final catch-all app.use((err, req, res, next) => { res.status(err.status || 500).json({ error: err.message }); });
404 Handler
A 404 is not an error — it's a successful response with no match. Register a regular middleware after all routes.
// 404 — after all routes, before error handler app.use((req, res, next) => { res.status(404).json({ error: { code: 'NOT_FOUND', message: `Cannot ${req.method} ${req.path}` }, }); }); // Or: create an error and forward it to the error handler app.use((req, res, next) => { next(new NotFoundError(`Route ${req.method} ${req.path}`)); });
Calling next() in Error Handlers
app.use((err, req, res, next) => { if (res.headersSent) { return next(err); // delegate to default Express error handler if headers already sent } res.status(500).json({ error: err.message }); });
Never call
next(err)after you've already sent a response — it causes the "headers already sent" error.
Common Error Patterns
| Scenario | Status | Pattern |
|---|---|---|
| Missing auth token | 401 | next(new UnauthorizedError()) |
| Valid token, wrong role | 403 | next(new AppError('Forbidden', 403, 'FORBIDDEN')) |
| Resource not found | 404 | next(new NotFoundError('User')) |
| Duplicate unique key | 409 | Catch db error, rethrow as 409 |
| Body too large | 413 | Caught automatically by express.json size limit |
| Validation failure | 400/422 | next(new ValidationError(errors.array())) |
| Unhandled promise | 500 | Use async wrapper; Express 5 catches automatically |
| External API failure | 502/503 | Catch and rethrow with appropriate status |
Logging Errors
const winston = require('winston'); app.use((err, req, res, next) => { winston.error({ message: err.message, level: err.status < 500 ? 'warn' : 'error', status: err.status, method: req.method, path: req.originalUrl, ip: req.ip, userId: req.user?.id, }); res.status(err.status || 500).json({ error: err.message }); });