Framework Cookbooks

Express.js Error Middleware Complete Guide

Master Express.js error handling from the four-parameter middleware signature to async error propagation, custom error classes, and centralized response formatting.

Express Error Handling Architecture

Express.js treats error handling as a first-class concern. Errors flow through the middleware stack until they reach an error-handling middleware — a special function with four parameters: (err, req, res, next). All other middleware has three.

The key rule: call next(err) to skip to the nearest error handler. Calling next() without an argument moves to the next *regular* middleware.

Request → Middleware A → Middleware B → Error!
                                         ↓
                              Error Handler (err, req, res, next)

The Four-Parameter Error Middleware

Error middleware must be registered after all routes and regular middleware:

const express = require('express');
const app = express();

// Regular routes
app.get('/users/:id', (req, res, next) => {
  const user = db.find(req.params.id);
  if (!user) {
    const err = new Error('User not found');
    err.status = 404;
    return next(err);  // Jump to error handler
  }
  res.json(user);
});

// Error-handling middleware — must have exactly 4 params
app.use((err, req, res, next) => {
  const status = err.status || err.statusCode || 500;
  res.status(status).json({
    error: {
      message: err.message,
      status,
    },
  });
});

You can chain multiple error handlers. Call next(err) to pass control to the next one. This is useful for separating logging from response formatting:

// Logging error handler
app.use((err, req, res, next) => {
  console.error(err);
  next(err);  // Forward to formatter
});

// Formatting error handler
app.use((err, req, res, next) => {
  res.status(err.status || 500).json({ error: err.message });
});

Async Error Handling (Express 5 vs 4)

Express 4 does not catch rejected promises automatically. You must wrap every async route:

// Express 4: manual wrapper required
const asyncHandler = (fn) => (req, res, next) =>
  Promise.resolve(fn(req, res, next)).catch(next);

app.get('/posts', asyncHandler(async (req, res) => {
  const posts = await db.getPosts();
  res.json(posts);
}));

Express 5 (currently in release candidate) automatically wraps async route handlers — rejected promises call next(err) implicitly:

// Express 5: no wrapper needed
app.get('/posts', async (req, res) => {
  const posts = await db.getPosts();  // Rejection → next(err) automatically
  res.json(posts);
});

Until Express 5 is stable, the express-async-errors package patches Express 4 to provide the same behavior: require('express-async-errors');

Custom Error Classes

Define a hierarchy of typed errors to carry status codes and machine-readable codes:

class AppError extends Error {
  constructor(message, status = 500, code = 'internal_error') {
    super(message);
    this.status = status;
    this.code = code;
    this.name = this.constructor.name;
    Error.captureStackTrace(this, this.constructor);
  }
}

class NotFoundError extends AppError {
  constructor(resource = 'Resource') {
    super(`${resource} not found`, 404, 'not_found');
  }
}

class ValidationError extends AppError {
  constructor(errors) {
    super('Validation failed', 422, 'validation_error');
    this.errors = errors;
  }
}

class UnauthorizedError extends AppError {
  constructor() {
    super('Authentication required', 401, 'unauthorized');
  }
}

Now in routes, throw clean domain errors:

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

Centralized Error Response Format

Adopt the RFC 9457 Problem Details format for a consistent API contract:

app.use((err, req, res, next) => {
  const status = err.status || 500;
  const isProduction = process.env.NODE_ENV === 'production';

  const body = {
    type: `https://protocolcodes.com/http/${status}`,
    title: err.name || 'Error',
    status,
    detail: err.message,
    instance: req.path,
  };

  // Add validation errors if present
  if (err.errors) body.errors = err.errors;

  // Omit stack trace in production
  if (!isProduction) body.stack = err.stack;

  res.status(status).json(body);
});

Logging and Monitoring Errors

Separate error logging from error response formatting. Use a logging middleware that fires before the response handler:

const { createLogger, format, transports } = require('winston');

const logger = createLogger({
  format: format.combine(format.timestamp(), format.json()),
  transports: [new transports.Console()],
});

// Logging middleware (first error handler)
app.use((err, req, res, next) => {
  const level = (err.status || 500) >= 500 ? 'error' : 'warn';
  logger[level]({
    message: err.message,
    status: err.status,
    path: req.path,
    method: req.method,
    requestId: req.headers['x-request-id'],
  });
  next(err);
});

A key principle: log 5xx errors as error level (pages on-call engineers) and 4xx errors as warn level (client mistakes, not server bugs).

Protocolos relacionados

Termos do glossário relacionados

Mais em Framework Cookbooks