Claude Code for Error Handling: Patterns That Don't Mask Bugs — Claude Skills 360 Blog
Blog / Development / Claude Code for Error Handling: Patterns That Don't Mask Bugs
Development

Claude Code for Error Handling: Patterns That Don't Mask Bugs

Published: July 9, 2026
Read time: 8 min read
By: Claude Skills 360

Most error handling code either ignores errors or catches them too broadly and hides bugs. Good error handling distinguishes between operational errors (network timeout, user not found) and programmer errors (null dereference, type mismatch), handles them differently, and makes the system fail loudly on the things that shouldn’t happen. Claude Code generates error handling that follows these principles instead of the common try { ... } catch (e) { console.log(e) } antipattern.

This guide covers error handling with Claude Code: Result types, error classification, centralized tracking, and retry logic.

Operational vs Programmer Errors

Help me understand what errors I should catch vs let crash.

Operational errors — expected, transitable:

  • Network requests fail (timeout, 503)
  • User input is invalid
  • Record not found in database
  • Third-party API rate limiting

→ Handle with fallbacks, user-friendly messages, retry logic

Programmer errors — bugs that should never happen:

  • TypeError: Cannot read property of null
  • RangeError: Maximum call stack exceeded
  • Assertion violations (assert(userId !== null, 'userId required'))

→ Let crash in production, fix in code. Don’t swallow with try/catch.

// Wrong — catches everything, including bugs
try {
  const user = users[userId]; // What if users is null?
  const result = processUser(user);
  return result;
} catch (e) {
  logger.error('error', e);
  return null; // Bug is now hidden
}

// Right — catch only what you expect
async function getUser(userId: string): Promise<User | null> {
  try {
    return await db.users.findById(userId);
  } catch (error) {
    if (error instanceof DatabaseNotFoundError) return null;
    if (error instanceof DatabaseConnectionError) throw error; // Let caller handle
    throw error; // Unexpected error — don't swallow
  }
}

Result Type Pattern

We want to avoid exceptions for expected failure cases.
Use a Result type so callers are forced to handle errors.
// src/lib/result.ts
export type Result<T, E = Error> = 
  | { ok: true; value: T }
  | { ok: false; error: E };

export function ok<T>(value: T): Result<T, never> {
  return { ok: true, value };
}

export function err<E>(error: E): Result<never, E> {
  return { ok: false, error };
}

// Helper: run async function, return Result instead of throwing
export async function tryAsync<T>(fn: () => Promise<T>): Promise<Result<T, Error>> {
  try {
    return ok(await fn());
  } catch (error) {
    return err(error instanceof Error ? error : new Error(String(error)));
  }
}
// Domain-specific error types
export class ValidationError extends Error {
  constructor(
    message: string,
    public readonly field: string,
    public readonly code: string,
  ) {
    super(message);
    this.name = 'ValidationError';
  }
}

export class NotFoundError extends Error {
  constructor(resource: string, id: string) {
    super(`${resource} not found: ${id}`);
    this.name = 'NotFoundError';
  }
}

export class ConflictError extends Error {
  constructor(message: string, public readonly conflictingField?: string) {
    super(message);
    this.name = 'ConflictError';
  }
}
// Service returns Result — caller is forced to check
async function createUser(data: CreateUserInput): Promise<Result<User, ValidationError | ConflictError>> {
  // Input validation
  if (!data.email.includes('@')) {
    return err(new ValidationError('Invalid email format', 'email', 'INVALID_FORMAT'));
  }
  
  const existing = await db.users.findByEmail(data.email);
  if (existing) {
    return err(new ConflictError('Email already registered', 'email'));
  }
  
  const user = await db.users.create(data);
  return ok(user);
}

// Route handler — must handle both cases
app.post('/users', async (req, res) => {
  const result = await createUser(req.body);
  
  if (!result.ok) {
    if (result.error instanceof ValidationError) {
      return res.status(422).json({
        error: 'Validation failed',
        field: result.error.field,
        code: result.error.code,
      });
    }
    if (result.error instanceof ConflictError) {
      return res.status(409).json({ error: result.error.message });
    }
    throw result.error; // Unexpected — let error handler deal with it
  }
  
  res.status(201).json({ user: result.value });
});

Retry Logic with Backoff

API calls to our payment provider fail occasionally.
Add automatic retry with exponential backoff.
// src/lib/retry.ts
interface RetryOptions {
  maxAttempts?: number;
  initialDelayMs?: number;
  maxDelayMs?: number;
  backoffMultiplier?: number;
  retryOn?: (error: Error) => boolean;
}

export async function withRetry<T>(
  fn: () => Promise<T>,
  options: RetryOptions = {},
): Promise<T> {
  const {
    maxAttempts = 3,
    initialDelayMs = 1000,
    maxDelayMs = 30_000,
    backoffMultiplier = 2,
    retryOn = isRetryableError,
  } = options;
  
  let lastError: Error;
  
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error instanceof Error ? error : new Error(String(error));
      
      if (attempt === maxAttempts || !retryOn(lastError)) {
        throw lastError;
      }
      
      // Exponential backoff with jitter
      const delay = Math.min(
        initialDelayMs * Math.pow(backoffMultiplier, attempt - 1),
        maxDelayMs,
      );
      const jitter = delay * 0.25 * Math.random(); // ±25% jitter
      
      console.warn(`Attempt ${attempt} failed, retrying in ${Math.round(delay + jitter)}ms:`, lastError.message);
      
      await sleep(delay + jitter);
    }
  }
  
  throw lastError!;
}

function isRetryableError(error: Error): boolean {
  // Network errors
  if (error.name === 'FetchError' || error.message.includes('ECONNREFUSED')) return true;
  if (error.message.includes('ETIMEDOUT') || error.message.includes('ENOTFOUND')) return true;
  
  // HTTP status codes that indicate transient issues
  if ('statusCode' in error) {
    const status = (error as any).statusCode;
    return [429, 503, 504].includes(status);
  }
  
  return false;
}

// Usage
const charge = await withRetry(
  () => stripe.charges.create({ amount, currency: 'usd', source: token }),
  {
    maxAttempts: 3,
    initialDelayMs: 1000,
    retryOn: (error) => {
      // Don't retry card declines (they won't change)
      if (error.message.includes('card_declined')) return false;
      return isRetryableError(error);
    },
  },
);

Centralized Error Tracking

We lose errors in logs. Set up Sentry with context
so we know what the user was doing when it broke.
// src/lib/error-tracker.ts
import * as Sentry from '@sentry/node';

export function initErrorTracking() {
  Sentry.init({
    dsn: process.env.SENTRY_DSN,
    environment: process.env.NODE_ENV,
    release: process.env.SERVICE_VERSION,
    
    // Don't track operational errors — only bugs
    beforeSend(event, hint) {
      const error = hint?.originalException;
      if (!error) return event;
      
      // Skip known operational errors
      if (error instanceof ValidationError) return null;
      if (error instanceof NotFoundError) return null;
      if (error instanceof ConflictError) return null;
      
      return event;
    },
  });
}

// Express error handler — catches all unhandled errors
export function errorHandler(err: Error, req: Request, res: Response, next: NextFunction) {
  // Add request context to Sentry
  Sentry.withScope((scope) => {
    scope.setUser({ id: req.user?.id, email: req.user?.email });
    scope.setContext('request', {
      method: req.method,
      path: req.path,
      query: req.query,
      body: req.body, // Caution: sanitize sensitive fields
    });
    
    Sentry.captureException(err);
  });
  
  // Return appropriate HTTP status
  if (err instanceof ValidationError) {
    return res.status(422).json({ error: err.message, field: err.field });
  }
  
  if (err instanceof NotFoundError) {
    return res.status(404).json({ error: err.message });
  }
  
  // Unknown error — don't expose details
  console.error('Unhandled error:', err);
  return res.status(500).json({ error: 'An unexpected error occurred' });
}

For React error boundaries that catch rendering errors, see the Next.js App Router guide which covers the error.tsx pattern. For testing error scenarios — ensuring error handling works as expected — see the testing strategies guide. The Claude Skills 360 bundle includes error handling skill sets for TypeScript applications. Start with the free tier to add Result types and centralized error tracking to your project.

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free