Skip to content

Express Error Handling — Complete Guide to Error Management in Express

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about Express Error Handling. We cover key concepts, practical examples, and best practices to help you master this topic.

Express error handling uses centralized error middleware to catch and respond to errors consistently, preventing crashes and providing clear feedback to clients.

What You'll Learn

By the end of this tutorial, you'll implement centralized Express error handling, create custom error classes, wrap async route handlers, handle 404s, and differentiate dev/production error responses.

Why Error Handling Matters

Without centralized error handling, every route must duplicate error logic. An unhandled async error crashes the server. Consistent error handling improves debugging and user experience.

Real-World Use

An Express API with 50 routes has one error middleware. When any route throws an error, the middleware logs the details, sanitizes the response, and returns a consistent JSON error format.

Express Error Handling Learning Path

flowchart LR
  A[Middleware] --> B[Error Handling]
  B --> C[Template Engines]
  C --> D[Sessions]
  D --> E[Security]
  A --> F{You Are Here}
  style F fill:#f90,color:#fff

The Problem: Async Route Errors

// This crashes the server on error
app.get("/users/:id", async (req, res) => {
  const user = await db.findUser(req.params.id);  // If this throws...
  res.json(user);
});

Express does not catch promise rejections. An unhandled rejection in async route handlers crashes the server.

Wrapper for Async Handlers

const asyncHandler = (fn) => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

app.get("/users/:id", asyncHandler(async (req, res) => {
  const user = await db.findUser(req.params.id);
  if (!user) throw new NotFoundError("User");
  res.json(user);
}));

Custom Error Classes

class AppError extends Error {
  constructor(message, statusCode = 500) {
    super(message);
    this.statusCode = statusCode;
    this.isOperational = true;
    Error.captureStackTrace(this, this.constructor);
  }
}
class NotFoundError extends AppError {
  constructor(resource = "Resource") {
    super(`${resource} not found`, 404);
  }
}
class ValidationError extends AppError {
  constructor(message) {
    super(message, 400);
  }
}

Centralized Error Middleware

// Must be registered AFTER all routes
app.use((err, req, res, next) => {
  err.statusCode = err.statusCode || 500;
  err.status = err.status || "error";

  if (process.env.NODE_ENV === "development") {
    res.status(err.statusCode).json({
      status: err.status,
      error: err,
      message: err.message,
      stack: err.stack
    });
  } else {
    res.status(err.statusCode).json({
      status: err.status,
      message: err.isOperational ? err.message : "Something went wrong"
    });
  }
});

404 Handler

// Must be after all routes but before error middleware
app.all("*", (req, res) => {
  res.status(404).json({
    status: "fail",
    message: `Route ${req.originalUrl} not found`
  });
});

Global Uncaught Handlers

process.on("uncaughtException", (err) => {
  console.error("UNCAUGHT EXCEPTION. Shutting down...");
  console.error(err.name, err.message);
  process.exit(1);
});
process.on("unhandledRejection", (err) => {
  console.error("UNHANDLED REJECTION. Shutting down...");
  console.error(err.name, err.message);
  server.close(() => process.exit(1));
});

Common Mistakes

1. Not Catching Async Errors

Async route errors without catch crash the server. Use asyncHandler wrapper for all async routes.

2. Error Middleware Before Routes

Error middleware must be registered after all routes. Otherwise, it won't catch route errors.

3. Leaking Error Details in Production

Stack traces and internal messages expose application internals. Return generic messages to clients in production.

4. Duplicate Error Handling

Don't add try/catch in every route when you have centralized error middleware. Throw errors and let middleware handle them.

5. Not Differentiating Error Types

Use custom error classes to distinguish validation errors (400), auth errors (401), not found (404), and server errors (500).

Practice Questions

1. Why doesn't Express catch async errors in route handlers?

Express only catches synchronous throws. Async errors return rejected promises that need explicit handling with .catch(next) or async wrapper.

2. What is the purpose of the asyncHandler wrapper?

It catches promise rejections and passes them to next(), which forwards them to Express error middleware.

3. How is error middleware different from regular middleware?

It has four parameters (err, req, res, next). Express identifies it by the function signature.

4. What should the 404 handler do?

Catch requests that don't match any route and return a 404 response with clear messaging about the invalid path.

5. Challenge: Create an Express app with centralized error handling for a simple API.

import express from "express";
const app = express();
const asyncHandler = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
class NotFoundError extends Error { constructor(m) { super(m); this.statusCode = 404; } }
app.get("/users/:id", asyncHandler(async (req, res) => {
  throw new NotFoundError("User not found");
}));
app.all("*", (req, res) => res.status(404).json({ error: "Not found" }));
app.use((err, req, res, next) => {
  res.status(err.statusCode || 500).json({ error: err.message || "Server error" });
});
app.listen(3000);

FAQ

Should I use try/catch or asyncHandler?

Both work, but asyncHandler reduces boilerplate. Each route doesn't need its own try/catch block.

How do I handle MongoDB/Mongoose errors?

Create error middleware that checks error.name === 'CastError', 'ValidationError', 'MongoServerError' and returns appropriate status codes.

What is the difference between operational and programmer errors?

Operational errors (invalid input, DB timeout) are expected and handled gracefully. Programmer errors (undefined variable) cause the process to exit.

How do I log errors with context?

Use a logger like Winston. In error middleware, log the full error with request details (method, url, user ID).

Can I skip error middleware for certain routes?

Call next('route') to skip to the next route handler. For error middleware, don't call next(err) for valid requests.

Mini Project: Error Handler with Slack Notifications

Build error middleware that sends critical errors to Slack.

app.use(async (err, req, res, next) => {
  if (err.statusCode >= 500) {
    try {
      await fetch(process.env.SLACK_WEBHOOK_URL, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          text: `Server Error: ${err.message}\nPath: ${req.method} ${req.url}`
        })
      });
    } catch (slackErr) { console.error("Slack notification failed"); }
  }
  res.status(err.statusCode || 500).json({ error: err.message });
});

What's Next

Express Template Engines Express Sessions Express Security

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro