Skip to content

Error Handling Middleware Patterns — Complete Implementation Guide

DodaTech Updated 2026-06-28 5 min read

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

  1. Not registering error handler last — Error handlers registered before routes will not catch errors from those routes.

  2. Throwing non-Error objects — Always throw Error instances. Throwing a string loses the stack trace and prevents proper handling.

  3. Not distinguishing operational vs programmer errors — Operational errors (invalid input) should be handled gracefully. Programmer errors (typos) should crash and alert developers.

  4. Exposing stack traces in production — Stack traces reveal code structure. Always hide them in production responses.

  5. Swallowing errors silently — Catching errors without logging them makes debugging impossible. Always log errors before responding.

Practice Questions

  1. How does Express recognize error-handling middleware? By the four parameters: err, req, res, next. Express checks the function's length property.

  2. 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.

  3. Why should async route handlers be wrapped? Express 4 does not catch promise rejections. The wrapper catches them and forwards to the error handler.

  4. 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

Should I catch errors in every route handler?

No. Let errors propagate to the global error handler. Only catch errors locally if you need to handle them specifically before re-throwing.

How do I handle 404 errors?

Add a catch-all middleware after all routes that creates a 404 error and passes it to the error handler.

What is the difference between operational and programmer errors?

Operational errors are expected (invalid input, DB down). Programmer errors are bugs (typo, undefined variable). Handle operational errors; fix programmer errors.

Can I have multiple error handling middleware?

Yes. Express runs them in order until one sends a response. Use this for category-specific error handling.

How do I log errors with different severity levels?

Check the error type and status code. 4xx errors are warnings. 5xx errors are critical and should trigger alerts.

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