Claude Skills Guide

Claude Code Express Middleware Error Handling Patterns Guide

Building reliable Express applications requires solid error handling strategies Whether you’re building APIs or full-stack applications with Claude Code, understanding middleware error handling patterns prevents unexpected crashes and provides meaningful feedback to users.

This guide covers practical Express middleware error handling patterns that work cleanly with Claude Code workflows. You’ll find code examples compatible with Express 4.x and 5.x, plus tips on integrating these patterns into your development process.

The Basics: Express Error Handling Middleware

Express provides a special middleware type specifically for error handling. Unlike regular middleware that takes three parameters (req, res, next), error handling middleware receives four: err, req, res, next.

// Basic error handling middleware
app.use((err, req, res, next) => {
  console.error('Error occurred:', err.message);
  
  const statusCode = err.statusCode || 500;
  const message = err.message || 'Internal Server Error';
  
  res.status(statusCode).json({
    error: {
      message,
      ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
    }
  });
});

This pattern catches errors from anywhere in your middleware chain. Place it last in your middleware stack to ensure it catches all unhandled errors.

Creating Custom Error Classes

Rather than throwing generic Error objects, create custom error classes that carry meaningful metadata. This pattern improves debugging and allows granular error handling.

// errors/AppError.js
class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
    this.isOperational = true;
    Error.captureStackTrace(this, this.constructor);
  }
}

class ValidationError extends AppError {
  constructor(message) {
    super(message, 400);
    this.name = 'ValidationError';
  }
}

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

class UnauthorizedError extends AppError {
  constructor(message = 'Authentication required') {
    super(message, 401);
    this.name = 'UnauthorizedError';
  }
}

module.exports = { AppError, ValidationError, NotFoundError, UnauthorizedError };

Using these custom errors in your routes provides clear, consistent error handling:

const { ValidationError, NotFoundError } = require('./errors/AppError');

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

Async Error Handling Wrapper

Since Express doesn’t automatically catch errors from async route handlers, you’ll encounter unhandled promise rejections. Create a wrapper function to handle this automatically:

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

// Usage with route handlers
app.get('/api/products', asyncHandler(async (req, res) => {
  const products = await Product.find();
  res.json(products);
}));

For TypeScript projects, here’s a typed version:

import { Request, Response, NextFunction, RequestHandler } from 'express';

const asyncHandler = (
  fn: (req: Request, res: Response, next: NextFunction) => Promise<any>
): RequestHandler => {
  return (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
};

Structured Error Response Format

Consistent error responses across your API improve client integration. Define a standard format:

// middleware/errorResponse.js
const errorResponse = (err, req, res, next) => {
  const response = {
    success: false,
    error: {
      type: err.name || 'Error',
      message: err.message,
      ...(err.errors && { validationErrors: err.errors })
    }
  };

  if (process.env.NODE_ENV === 'development') {
    response.error.stack = err.stack;
    response.error.details = err;
  }

  res.status(err.statusCode || 500).json(response);
};

module.exports = errorResponse;

Middleware Chaining with Error-First Callbacks

When integrating multiple middleware pieces, handle errors through proper chaining. This pattern works well with authentication and validation middleware:

// Authentication middleware that passes errors to next()
const authenticate = async (req, res, next) => {
  try {
    const token = req.headers.authorization?.split(' ')[1];
    
    if (!token) {
      return next(new UnauthorizedError('No token provided'));
    }
    
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch (error) {
    next(new UnauthorizedError('Invalid token'));
  }
};

// Protected route using middleware chain
app.post('/api/orders', 
  validateOrderInput,
  authenticate,
  asyncHandler(async (req, res) => {
    const order = await Order.create({
      ...req.body,
      userId: req.user.id
    });
    res.status(201).json(order);
  })
);

Graceful Shutdown and Error Logging

Production applications need proper error logging and graceful shutdown handling:

// middleware/errorLogger.js
const errorLogger = (err, req, res, next) => {
  const logData = {
    timestamp: new Date().toISOString(),
    method: req.method,
    url: req.url,
    body: req.body,
    error: {
      name: err.name,
      message: err.message,
      stack: err.stack
    },
    user: req.user?.id || 'anonymous'
  };
  
  if (err.statusCode >= 500) {
    console.error('Server Error:', JSON.stringify(logData, null, 2));
    // Send to logging service (Sentry, Datadog, etc.)
  } else {
    console.warn('Client Error:', JSON.stringify(logData, null, 2));
  }
  
  next(err);
};

module.exports = errorLogger;

For graceful shutdown, ensure your Express server handles uncaught exceptions:

const server = app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Rejection:', reason);
  server.close(() => process.exit(1));
});

process.on('uncaughtException', (error) => {
  console.error('Uncaught Exception:', error);
  server.close(() => process.exit(1));
});

Integrating with Claude Code Workflows

When building Express applications with Claude Code, these error handling patterns integrate naturally with your development workflow. The tdd skill helps you write tests for error scenarios before implementing handlers, ensuring your error paths work correctly.

For API documentation, the pdf skill can generate API reference documents that include error response schemas. This helps teams understand possible error codes and messages.

The supermemory skill preserves context about your error handling decisions across Claude Code sessions, making it easier to maintain consistent patterns across larger projects.

If you’re building forms that submit to Express backends, combine these patterns with the frontend-design skill to create user-friendly error displays that match your validation logic.

Summary

Effective Express middleware error handling requires several interconnected patterns:

  1. Custom error classes provide semantic meaning and consistent status codes
  2. Async wrappers prevent unhandled promise rejections
  3. Structured responses give clients predictable error formats
  4. Proper middleware ordering ensures errors reach handlers correctly
  5. Logging and shutdown handle critical errors gracefully

These patterns scale from small APIs to enterprise applications. Implement them early in your project to avoid retrofitting error handling later.

Built by theluckystrike — More at zovo.one