Express Cheatsheet
Error Handling
Use this Express reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.
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 }); });