DEV Community

myougaTheAxo
myougaTheAxo

Posted on

Error Handling with Claude Code: Custom Error Classes and Centralized Error Management

When you handle errors ad-hoc in each route, you end up with inconsistent response formats, stack traces leaking to clients, and no way to tell which errors are bugs versus expected failures. Claude Code can generate a centralized error management system that solves all three.


The CLAUDE.md Setup

Add this to your CLAUDE.md to get consistent error handling across every endpoint:

## Error Handling

- AppError base class with: statusCode, code (machine-readable string), isOperational flag
- Subtypes: NotFoundError(404), UnauthorizedError(401), ForbiddenError(403),
  ValidationError(422), ConflictError(409), RateLimitError(429)
- isOperational=false = unexpected bug → send urgent Slack alert
- Client response: never include stack trace in production; always include error.code
- Logging: 4xx = warn, 5xx = error + Slack notification
Enter fullscreen mode Exit fullscreen mode

With this in place, Claude Code knows which error classes to create, when to alert, and how to respond to clients.


AppError Base Class

export class AppError extends Error {
  public readonly statusCode: number;
  public readonly code: string;
  public readonly isOperational: boolean;
  public readonly details?: unknown;

  constructor(
    message: string,
    statusCode: number,
    code: string,
    isOperational = true,
    details?: unknown
  ) {
    super(message);
    this.statusCode = statusCode;
    this.code = code;
    this.isOperational = isOperational;
    this.details = details;
    Error.captureStackTrace(this, this.constructor);
  }
}
Enter fullscreen mode Exit fullscreen mode

The isOperational flag is the key design decision. Operational errors (404, 401, 422) are expected — they happen because of client input. Non-operational errors are bugs — something broke that shouldn't have. The errorHandler treats them differently.


All Error Subclasses

export class NotFoundError extends AppError {
  constructor(resource: string, id?: string) {
    const message = id
      ? `${resource} with id '${id}' not found`
      : `${resource} not found`;
    super(message, 404, 'NOT_FOUND');
  }
}

export class UnauthorizedError extends AppError {
  constructor(message = 'Authentication required') {
    super(message, 401, 'UNAUTHORIZED');
  }
}

export class ForbiddenError extends AppError {
  constructor(message = 'Access denied') {
    super(message, 403, 'FORBIDDEN');
  }
}

export class ValidationError extends AppError {
  constructor(message: string, details?: unknown) {
    super(message, 422, 'VALIDATION_ERROR', true, details);
  }
}

export class ConflictError extends AppError {
  constructor(message: string) {
    super(message, 409, 'CONFLICT');
  }
}

export class RateLimitError extends AppError {
  constructor(message = 'Too many requests') {
    super(message, 429, 'RATE_LIMIT_EXCEEDED');
  }
}
Enter fullscreen mode Exit fullscreen mode

Each subclass sets a fixed statusCode and code. No magic numbers scattered through route handlers — just throw new NotFoundError('User', req.params.id).


Centralized errorHandler Middleware

import { Request, Response, NextFunction } from 'express';
import { AppError } from './errors';
import { logger } from './logger';
import { notifySlack } from './slack';

export function errorHandler(
  err: Error,
  req: Request,
  res: Response,
  next: NextFunction
): void {
  if (err instanceof AppError) {
    // Operational error: expected failure
    if (err.statusCode >= 500) {
      logger.error({ err, req: { method: req.method, url: req.url } });
      notifySlack(`[ERROR] ${err.code}: ${err.message}`);
    } else {
      logger.warn({ err, req: { method: req.method, url: req.url } });
    }

    res.status(err.statusCode).json({
      error: {
        code: err.code,
        message: err.message,
        ...(err.details ? { details: err.details } : {}),
        ...(process.env.NODE_ENV !== 'production' ? { stack: err.stack } : {}),
      },
    });
    return;
  }

  // Unexpected error: a bug
  logger.fatal({ err, req: { method: req.method, url: req.url } });
  notifySlack(`[FATAL] Unexpected error: ${err.message}`);

  res.status(500).json({
    error: {
      code: 'INTERNAL_ERROR',
      message: 'An unexpected error occurred',
    },
  });
}
Enter fullscreen mode Exit fullscreen mode

The error.code field is always present in the response. Frontend code can branch on error.code without parsing error messages — which is fragile and breaks whenever you reword a message.


asyncHandler Wrapper

Express 4 doesn't catch async errors automatically. You need a wrapper:

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

export function asyncHandler(
  fn: (req: Request, res: Response, next: NextFunction) => Promise<void>
): RequestHandler {
  return (req, res, next) => {
    fn(req, res, next).catch(next);
  };
}
Enter fullscreen mode Exit fullscreen mode

Or install express-async-errors and import it once at the top of your app — it patches Express to handle async errors automatically.


Usage in Routes

import { asyncHandler } from './asyncHandler';
import { NotFoundError, ConflictError } from './errors';

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

// POST /users
router.post('/users', asyncHandler(async (req, res) => {
  const existing = await userRepository.findByEmail(req.body.email);
  if (existing) {
    throw new ConflictError(`Email '${req.body.email}' is already registered`);
  }
  const user = await userRepository.create(req.body);
  res.status(201).json(user);
}));
Enter fullscreen mode Exit fullscreen mode

No try/catch in the route. No res.status(404).json({ message: '...' }) scattered everywhere. Just throw the right error class and the middleware handles the rest.


Summary

  • Define the error hierarchy in CLAUDE.md — Claude Code generates it consistently across all handlers
  • AppError subclasses (NotFoundError, ValidationError, etc.) replace magic numbers
  • Centralized errorHandler middleware handles logging, Slack alerting, and response format
  • isOperational flag separates expected failures from bugs — bugs trigger urgent alerts
  • asyncHandler wrapper or express-async-errors ensures async throws reach the middleware

Want Claude Code to review your existing error handling for security gaps and response format inconsistencies?

Code Review Pack (¥980) /code-review — includes prompts for error handling audit, stack trace exposure detection, and response format consistency checks.

Top comments (0)