Error Handling & Production Patterns
📖 Concept
Robust error handling is what separates amateur Express apps from production-ready ones. Express has a specific mechanism for centralizing error handling across all routes.
Express error-handling flow:
- A route handler throws or calls
next(err) - Express skips all remaining non-error middleware
- Express calls the first error-handling middleware (
(err, req, res, next))
Error-handling middleware signature:
app.use((err, req, res, next) => {
// Handle the error
// Must have EXACTLY 4 parameters!
});
Production error handling strategy:
- Custom error classes — carry status codes, operational flags, context
- Async wrapper — automatically catch async errors and pass to
next() - Centralized error handler — single middleware that formats all errors
- Error logging — structured logging with request context
- Graceful shutdown — close connections cleanly on unhandled errors
Operational vs. Programmer errors:
| Type | Example | Action |
|---|---|---|
| Operational | Invalid input, not found, timeout | Return appropriate HTTP error |
| Programmer | TypeError, null reference | Log, alert, restart process |
🏠 Real-world analogy: Error handling is like a hospital triage system. Minor issues (400 errors) are treated and released. Serious issues (500 errors) are logged and escalated. Catastrophic events (unhandled exceptions) trigger emergency protocols (graceful shutdown + restart).
💻 Code Example
1// Production Error Handling in Express23const express = require("express");4const app = express();56// 1. Custom error classes7class AppError extends Error {8 constructor(message, statusCode) {9 super(message);10 this.statusCode = statusCode;11 this.isOperational = true;12 Error.captureStackTrace(this, this.constructor);13 }14}1516class NotFoundError extends AppError {17 constructor(resource = "Resource") {18 super(`${resource} not found`, 404);19 }20}2122class ValidationError extends AppError {23 constructor(errors) {24 super("Validation failed", 400);25 this.errors = errors;26 }27}2829class UnauthorizedError extends AppError {30 constructor(message = "Authentication required") {31 super(message, 401);32 }33}3435// 2. Async wrapper — eliminates try/catch boilerplate36const asyncHandler = (fn) => (req, res, next) => {37 Promise.resolve(fn(req, res, next)).catch(next);38};3940// 3. Routes using custom errors and async wrapper41app.use(express.json());4243app.get(44 "/api/users/:id",45 asyncHandler(async (req, res) => {46 const id = parseInt(req.params.id);47 if (isNaN(id)) throw new ValidationError([{ field: "id", message: "Must be a number" }]);4849 // Simulate database lookup50 const user = id === 1 ? { id: 1, name: "Alice" } : null;51 if (!user) throw new NotFoundError("User");5253 res.json({ data: user });54 })55);5657app.post(58 "/api/users",59 asyncHandler(async (req, res) => {60 const { name, email } = req.body;61 const errors = [];62 if (!name) errors.push({ field: "name", message: "Required" });63 if (!email) errors.push({ field: "email", message: "Required" });64 if (errors.length) throw new ValidationError(errors);6566 const user = { id: Date.now(), name, email };67 res.status(201).json({ data: user });68 })69);7071// 4. 404 handler (not an error handler — no err parameter)72app.use((req, res, next) => {73 next(new NotFoundError(`Route ${req.method} ${req.path}`));74});7576// 5. Centralized error handler (4 parameters!)77app.use((err, req, res, next) => {78 // Default values for non-AppError errors79 err.statusCode = err.statusCode || 500;80 err.message = err.isOperational ? err.message : "Internal server error";8182 // Log error (structured for log aggregation)83 const logEntry = {84 timestamp: new Date().toISOString(),85 level: err.statusCode >= 500 ? "error" : "warn",86 message: err.message,87 statusCode: err.statusCode,88 method: req.method,89 path: req.originalUrl,90 ip: req.ip,91 userId: req.user?.id,92 requestId: req.headers["x-request-id"],93 ...(err.statusCode >= 500 && { stack: err.stack }),94 };95 console.error(JSON.stringify(logEntry));9697 // Send response98 const response = {99 success: false,100 error: err.message,101 ...(err instanceof ValidationError && { details: err.errors }),102 ...(process.env.NODE_ENV === "development" && { stack: err.stack }),103 };104105 res.status(err.statusCode).json(response);106});107108// 6. Global error handlers — safety nets109process.on("unhandledRejection", (reason, promise) => {110 console.error("UNHANDLED REJECTION:", reason);111 // Treat like uncaughtException — shut down112 process.exit(1);113});114115process.on("uncaughtException", (error) => {116 console.error("UNCAUGHT EXCEPTION:", error);117 // Graceful shutdown (close server, then exit)118 process.exit(1);119});120121const PORT = process.env.PORT || 3000;122const server = app.listen(PORT, () => {123 console.log(`Server running on port ${PORT}`);124});125126// 7. Graceful shutdown127function shutdown(signal) {128 console.log(`${signal} received. Graceful shutdown...`);129 server.close(() => {130 console.log("HTTP server closed");131 // Close database connections, etc.132 process.exit(0);133 });134 setTimeout(() => {135 console.error("Forced shutdown");136 process.exit(1);137 }, 10000);138}139140process.on("SIGTERM", () => shutdown("SIGTERM"));141process.on("SIGINT", () => shutdown("SIGINT"));
🏋️ Practice Exercise
Exercises:
- Create a hierarchy of custom error classes: AppError → NotFoundError, ValidationError, UnauthorizedError, ForbiddenError
- Implement the
asyncHandlerwrapper and refactor all routes to use it - Build a centralized error handler that logs errors and sends appropriate JSON responses
- Add request ID tracking — generate an ID per request and include it in all error logs
- Implement graceful shutdown that waits for in-flight requests to complete before exiting
- Test error handling by triggering 400, 401, 403, 404, and 500 errors intentionally
⚠️ Common Mistakes
Not using error-handling middleware (4-parameter) — Express ignores your function for errors if it doesn't have exactly
(err, req, res, next)parametersForgetting to pass errors in async handlers —
async (req, res)without try/catch causes unhandled Promise rejections that crash Node.jsExposing stack traces in production — stack traces reveal file paths and code structure; only include in development mode
Not differentiating operational and programmer errors — a 404 should return a helpful message; a null reference should trigger alerts and process restart
Handling errors in every route instead of centralizing — this leads to inconsistent error formats; use a single error-handling middleware
💼 Interview Questions
🎤 Mock Interview
Mock interview is powered by AI for Error Handling & Production Patterns. Login to unlock this feature.