Claude
Skills
Sign in
Back

error-handling-patterns

Included with Lifetime
$97 forever

Error handling patterns including exceptions, Result pattern, validation strategies, retry logic, and circuit breakers. **ALWAYS use when implementing error handling in backend code, APIs, use cases, or validation logic.** Use proactively for robust error handling, recovery mechanisms, and failure scenarios. Examples - "handle errors", "Result pattern", "throw exception", "validate input", "error recovery", "retry logic", "circuit breaker", "exception hierarchy".

Backend & APIs

What this skill does


You are an expert in error handling patterns and strategies. You guide developers to implement robust, maintainable error handling that provides clear feedback and proper recovery mechanisms.

**For complete backend implementation examples using these error handling patterns (Clean Architecture layers, DI Container, Use Cases, Repositories), see `backend-engineer` skill**

## When to Engage

You should proactively assist when:

- Implementing error handling within bounded contexts
- Designing context-specific validation logic
- Creating context-specific exception types (no base classes)
- Implementing retry or recovery mechanisms per context
- User asks about error handling strategies
- Reviewing error handling without over-abstraction

## Modular Monolith Error Handling

### Context-Specific Errors (No Base Classes)

```typescript
// ❌ BAD: Base error class creates coupling
export abstract class DomainError extends Error {
  // Forces all contexts to use same error structure
}

// ✅ GOOD: Each context has its own errors
// contexts/auth/domain/errors/auth-validation.error.ts
export class AuthValidationError extends Error {
  constructor(message: string, public readonly field?: string) {
    super(message);
    this.name = "AuthValidationError";
  }
}

// contexts/tax/domain/errors/tax-calculation.error.ts
export class TaxCalculationError extends Error {
  constructor(message: string, public readonly ncmCode?: string) {
    super(message);
    this.name = "TaxCalculationError";
  }
}
```

### Error Handling Rules

1. **Each context owns its errors** - No shared error classes
2. **Duplicate error structures** - Better than coupling through inheritance
3. **Context-specific metadata** - Each error has relevant context data
4. **Simple over clever** - Avoid complex error hierarchies

## Core Principles

### 1. Use Exceptions, Not Return Codes

```typescript
// ✅ Good - Use exceptions with context
export class CreateUserUseCase {
  async execute(dto: CreateUserDto): Promise<User> {
    if (!this.isValidEmail(dto.email)) {
      throw new ValidationError("Invalid email format", {
        email: dto.email,
        field: "email",
      });
    }

    try {
      return await this.repository.save(user);
    } catch (error) {
      throw new DatabaseError("Failed to create user", {
        originalError: error,
        userId: user.id,
      });
    }
  }
}

// ❌ Bad - Return codes
export class CreateUserUseCase {
  async execute(dto: CreateUserDto): Promise<{
    success: boolean;
    user?: User;
    error?: string;
  }> {
    if (!this.isValidEmail(dto.email)) {
      return { success: false, error: "Invalid email" };
    }
    // Forces caller to check success flag everywhere
  }
}
```

### 2. Never Return Null for Errors

```typescript
// ✅ Good - Explicit optional with undefined
export class UserService {
  async findById(id: string): Promise<User | undefined> {
    return this.repository.findById(id);
  }

  // Or throw if must exist
  async getUserById(id: string): Promise<User> {
    const user = await this.repository.findById(id);
    if (!user) {
      throw new NotFoundError(`User ${id} not found`);
    }
    return user;
  }
}

// ❌ Bad - Returning null loses error context
export class UserService {
  async findById(id: string): Promise<User | null> {
    // Why null? Not found? Database error? Network error?
    return null;
  }
}
```

### 3. Provide Context with Exceptions

```typescript
// ✅ Good - Rich error context
export class ValidationError extends Error {
  constructor(
    message: string,
    public readonly context: Record<string, unknown>
  ) {
    super(message);
    this.name = "ValidationError";
  }
}

throw new ValidationError("Invalid email format", {
  email: dto.email,
  field: "email",
  rule: "email-format",
  attemptedAt: new Date().toISOString(),
});

// ❌ Bad - No context
throw new Error("Invalid");
```

## Exception Hierarchy

### Custom Domain Exceptions

```typescript
// Base domain exception
export abstract class DomainError extends Error {
  constructor(
    message: string,
    public readonly code: string,
    public readonly context?: Record<string, unknown>
  ) {
    super(message);
    this.name = this.constructor.name;
  }
}

// Specific domain exceptions
export class UserAlreadyExistsError extends DomainError {
  constructor(email: string) {
    super(`User with email ${email} already exists`, "USER_ALREADY_EXISTS", {
      email,
    });
  }
}

export class InvalidPasswordError extends DomainError {
  constructor(reason: string) {
    super("Password does not meet requirements", "INVALID_PASSWORD", {
      reason,
    });
  }
}

export class InsufficientPermissionsError extends DomainError {
  constructor(userId: string, resource: string, action: string) {
    super(
      `User ${userId} cannot ${action} ${resource}`,
      "INSUFFICIENT_PERMISSIONS",
      { userId, resource, action }
    );
  }
}
```

### Infrastructure Exceptions

```typescript
export class DatabaseError extends Error {
  constructor(
    message: string,
    public readonly originalError: unknown,
    public readonly query?: string
  ) {
    super(message);
    this.name = "DatabaseError";
  }
}

export class ExternalServiceError extends Error {
  constructor(
    public readonly service: string,
    message: string,
    public readonly statusCode?: number,
    public readonly originalError?: unknown
  ) {
    super(`${service}: ${message}`);
    this.name = "ExternalServiceError";
  }
}
```

## Result Pattern

For operations with expected failures:

```typescript
export type Result<T, E = Error> =
  | { success: true; value: T }
  | { success: false; error: E };

export class UserService {
  async findByEmail(email: string): Promise<Result<User, NotFoundError>> {
    const user = await this.repository.findByEmail(email);

    if (!user) {
      return {
        success: false,
        error: new NotFoundError("User not found"),
      };
    }

    return { success: true, value: user };
  }
}

// Usage
const result = await userService.findByEmail("[email protected]");

if (!result.success) {
  console.error("User not found:", result.error.message);
  return;
}

// TypeScript knows result.value is User here
const user = result.value;
```

### When to Use Result Pattern

**Use Result for:**

- Expected business failures (user not found, insufficient balance)
- Operations where failure is part of normal flow
- When caller needs to handle different failure types

**Use Exceptions for:**

- Unexpected errors (database connection lost, out of memory)
- Programming errors (invalid state, null pointer)
- Infrastructure failures

## Validation Patterns

### Input Validation at Boundaries

```typescript
import { z } from "zod";

// ✅ Good - Validate at system boundaries
const CreateUserSchema = z.object({
  email: z.string().email("Invalid email format"),
  password: z
    .string()
    .min(8, "Password must be at least 8 characters")
    .regex(/[A-Z]/, "Password must contain uppercase letter")
    .regex(/[0-9]/, "Password must contain number"),
  name: z
    .string()
    .min(2, "Name must be at least 2 characters")
    .max(100, "Name must be at most 100 characters"),
  age: z
    .number()
    .int("Age must be an integer")
    .min(18, "Must be at least 18 years old")
    .optional(),
});

export type CreateUserDto = z.infer<typeof CreateUserSchema>;

// In Hono controller
import { zValidator } from "@hono/zod-validator";

app.post("/users", zValidator("json", CreateUserSchema), async (c) => {
  const data = c.req.valid("json"); // Type-safe and validated
  const user = await createUserUseCase.execute(data);
  return c.json(user, 201);
});
```

### Domain Validation

```typescript
// ✅ Good - Validate in domain entities
export class Email {
  private constructor(private readonly value: string) {}

  static create(value: string): Result<Email, ValidationError> {
    if (!value) {
      return {
        success: false,
        error: new 

Related in Backend & APIs