Error Handling Middleware Patterns — Complete Implementation Guide
In this tutorial, you will learn about Error Handling Middleware Patterns. We cover key concepts, practical examples, and best practices to help you master this topic.
Error handling middleware patterns catch and Process errors from all middleware and route handlers, providing consistent error responses and preventing server crashes in production applications.
What You'll Learn
By the end of this tutorial, you will build error handling middleware that catches sync and async errors, creates custom error classes, and returns consistent error responses for your API.
Why It Matters
Unhandled errors crash servers and expose internal details to attackers. Proper error handling middleware keeps your application running and returns safe, informative error messages to clients.
Real-World Use
Durga Antivirus Pro's scan API uses error handling middleware that categorizes errors by type, logs them with full stack traces in development, and returns sanitized messages in production.
Error Middleware Learning Path
flowchart LR
A[Auth Middleware] --> B[Error Middleware]
B --> C[Custom Errors]
C --> D[Async Handling]
B --> E{You Are Here}
style E fill:#f90,color:#fff
Global Error Handler
Express identifies error-handling middleware by its four parameters: err, req, res, next. This middleware catches errors thrown or passed from any preceding middleware or route handler.
const express = require("express");
const app = express();
app.get("/error", (req, res) => {
throw new Error("Something broke");
});
app.use((err, req, res, next) => {
console.error("Unhandled error:", err.message);
res.status(500).json({
error: "Internal server error",
...(process.env.NODE_ENV === "development" && { details: err.message })
});
});
app.listen(3000);
Expected output for GET /error in development mode:
{"error": "Internal server error", "details": "Something broke"}
Expected output for GET /error in production mode:
{"error": "Internal server error"}
Custom Error Classes
Custom error classes let you attach status codes and metadata to errors, making it easy to return appropriate HTTP responses without duplicating logic.
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
}
}
class NotFoundError extends AppError {
constructor(resource = "Resource") {
super(`${resource} not found`, 404);
}
}
class ValidationError extends AppError {
constructor(message) {
super(message, 400);
}
}
const app = express();
app.use(express.json());
app.get("/users/:id", (req, res, next) => {
const user = findUser(req.params.id);
if (!user) {
throw new NotFoundError("User");
}
res.json(user);
});
app.put("/users/:id", (req, res, next) => {
if (!req.body.name) {
throw new ValidationError("Name is required");
}
res.json({ updated: true });
});
app.use((err, req, res, next) => {
const statusCode = err.statusCode || 500;
res.status(statusCode).json({
error: err.message,
type: err.constructor.name
});
});
function findUser(id) {
return null;
}
Expected output for GET /users/99:
{"error": "User not found", "type": "NotFoundError"}
Async Error Handling
Express 4 does not catch promise rejections automatically. Async error handling middleware requires wrapping async route handlers to forward errors.
const express = require("express");
const app = express();
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
app.get("/async-data", asyncHandler(async (req, res) => {
const data = await fetchData();
if (!data) {
throw new AppError("Data not available", 404);
}
res.json(data);
}));
function fetchData() {
return Promise.resolve(null);
}
app.use((err, req, res, next) => {
console.error("Async error:", err.message);
res.status(err.statusCode || 500).json({ error: err.message });
});
Expected output for GET /async-data:
{"error": "Data not available"}
Common Mistakes
Not registering error handler last — Error handlers registered before routes will not catch errors from those routes.
Throwing non-Error objects — Always throw Error instances. Throwing a string loses the stack trace and prevents proper handling.
Not distinguishing operational vs programmer errors — Operational errors (invalid input) should be handled gracefully. Programmer errors (typos) should crash and alert developers.
Exposing stack traces in production — Stack traces reveal code structure. Always hide them in production responses.
Swallowing errors silently — Catching errors without logging them makes debugging impossible. Always log errors before responding.
Practice Questions
How does Express recognize error-handling middleware? By the four parameters: err, req, res, next. Express checks the function's length property.
What happens if error middleware throws an error? Express catches it and passes it to the next error handler. If none exists, the server responds with a generic 500.
Why should async route handlers be wrapped? Express 4 does not catch promise rejections. The wrapper catches them and forwards to the error handler.
Challenge: Create an error handler that returns different responses based on environment.
app.use((err, req, res, next) => {
const response = { error: "Internal server error" };
if (process.env.NODE_ENV === "development") {
response.stack = err.stack;
response.details = err.message;
}
res.status(err.statusCode || 500).json(response);
});
FAQ
Mini Project
Build a complete error handling system with custom error classes, async handler wrapper, and environment-aware error responses.
const express = require("express");
const app = express();
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
}
}
function asyncHandler(fn) {
return (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
}
function notFoundHandler(req, res, next) {
next(new AppError("Route not found", 404));
}
app.get("/ok", (req, res) => res.json({ status: "ok" }));
app.get("/fail", asyncHandler(async (req, res) => {
throw new AppError("Processing failed", 422);
}));
app.use(notFoundHandler);
app.use((err, req, res, next) => {
const statusCode = err.statusCode || 500;
const response = { error: err.message };
if (process.env.NODE_ENV === "development") {
response.stack = err.stack;
}
console.error(`[${statusCode}] ${err.message}`);
res.status(statusCode).json(response);
});
app.listen(3000);
What's Next
Now that you understand error handling middleware, explore validating request data in middleware. Then learn about compressing responses with middleware.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro