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:

  1. A route handler throws or calls next(err)
  2. Express skips all remaining non-error middleware
  3. 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:

  1. Custom error classes — carry status codes, operational flags, context
  2. Async wrapper — automatically catch async errors and pass to next()
  3. Centralized error handler — single middleware that formats all errors
  4. Error logging — structured logging with request context
  5. 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

codeTap to expand ⛶
1// Production Error Handling in Express
2
3const express = require("express");
4const app = express();
5
6// 1. Custom error classes
7class 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}
15
16class NotFoundError extends AppError {
17 constructor(resource = "Resource") {
18 super(`${resource} not found`, 404);
19 }
20}
21
22class ValidationError extends AppError {
23 constructor(errors) {
24 super("Validation failed", 400);
25 this.errors = errors;
26 }
27}
28
29class UnauthorizedError extends AppError {
30 constructor(message = "Authentication required") {
31 super(message, 401);
32 }
33}
34
35// 2. Async wrapper — eliminates try/catch boilerplate
36const asyncHandler = (fn) => (req, res, next) => {
37 Promise.resolve(fn(req, res, next)).catch(next);
38};
39
40// 3. Routes using custom errors and async wrapper
41app.use(express.json());
42
43app.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" }]);
48
49 // Simulate database lookup
50 const user = id === 1 ? { id: 1, name: "Alice" } : null;
51 if (!user) throw new NotFoundError("User");
52
53 res.json({ data: user });
54 })
55);
56
57app.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);
65
66 const user = { id: Date.now(), name, email };
67 res.status(201).json({ data: user });
68 })
69);
70
71// 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});
75
76// 5. Centralized error handler (4 parameters!)
77app.use((err, req, res, next) => {
78 // Default values for non-AppError errors
79 err.statusCode = err.statusCode || 500;
80 err.message = err.isOperational ? err.message : "Internal server error";
81
82 // 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));
96
97 // Send response
98 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 };
104
105 res.status(err.statusCode).json(response);
106});
107
108// 6. Global error handlers — safety nets
109process.on("unhandledRejection", (reason, promise) => {
110 console.error("UNHANDLED REJECTION:", reason);
111 // Treat like uncaughtException — shut down
112 process.exit(1);
113});
114
115process.on("uncaughtException", (error) => {
116 console.error("UNCAUGHT EXCEPTION:", error);
117 // Graceful shutdown (close server, then exit)
118 process.exit(1);
119});
120
121const PORT = process.env.PORT || 3000;
122const server = app.listen(PORT, () => {
123 console.log(`Server running on port ${PORT}`);
124});
125
126// 7. Graceful shutdown
127function 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}
139
140process.on("SIGTERM", () => shutdown("SIGTERM"));
141process.on("SIGINT", () => shutdown("SIGINT"));

🏋️ Practice Exercise

Exercises:

  1. Create a hierarchy of custom error classes: AppError → NotFoundError, ValidationError, UnauthorizedError, ForbiddenError
  2. Implement the asyncHandler wrapper and refactor all routes to use it
  3. Build a centralized error handler that logs errors and sends appropriate JSON responses
  4. Add request ID tracking — generate an ID per request and include it in all error logs
  5. Implement graceful shutdown that waits for in-flight requests to complete before exiting
  6. 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) parameters

  • Forgetting to pass errors in async handlers — async (req, res) without try/catch causes unhandled Promise rejections that crash Node.js

  • Exposing 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.