Error Handling System

Overview

PadawanForge implements a comprehensive error handling system that provides structured error management, standardized API responses, and robust error tracking. This system ensures consistent error handling across all application layers.

Architecture

Core Components

  • Structured Error Types: Predefined error codes and categories
  • API Error Standardization: Consistent error response format
  • Error Categorization: Automatic error classification
  • Request Tracking: End-to-end error tracing
  • Debug Information: Development-friendly error details

Error Categories

  • Authentication Errors: Unauthorized access, invalid tokens
  • Validation Errors: Invalid input, missing required fields
  • Resource Errors: Not found, conflicts, already exists
  • Database Errors: Connection issues, query failures
  • Server Errors: Internal errors, service unavailable
  • External Service Errors: API failures, rate limits
  • Business Logic Errors: Rule violations, insufficient permissions

Implementation

Error Types and Codes

import { ErrorCode, AppError, ValidationError, NotFoundError } from '@/lib/errors/types';

// Predefined error codes
enum ErrorCode {
  UNAUTHORIZED = 'UNAUTHORIZED',
  FORBIDDEN = 'FORBIDDEN',
  INVALID_TOKEN = 'INVALID_TOKEN',
  SESSION_EXPIRED = 'SESSION_EXPIRED',
  VALIDATION_ERROR = 'VALIDATION_ERROR',
  INVALID_INPUT = 'INVALID_INPUT',
  MISSING_REQUIRED_FIELD = 'MISSING_REQUIRED_FIELD',
  NOT_FOUND = 'NOT_FOUND',
  ALREADY_EXISTS = 'ALREADY_EXISTS',
  RESOURCE_CONFLICT = 'RESOURCE_CONFLICT',
  DATABASE_ERROR = 'DATABASE_ERROR',
  CONNECTION_ERROR = 'CONNECTION_ERROR',
  TRANSACTION_FAILED = 'TRANSACTION_FAILED',
  INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR',
  SERVICE_UNAVAILABLE = 'SERVICE_UNAVAILABLE',
  TIMEOUT = 'TIMEOUT',
  EXTERNAL_SERVICE_ERROR = 'EXTERNAL_SERVICE_ERROR',
  API_RATE_LIMIT = 'API_RATE_LIMIT',
  BUSINESS_RULE_VIOLATION = 'BUSINESS_RULE_VIOLATION',
  INSUFFICIENT_PERMISSIONS = 'INSUFFICIENT_PERMISSIONS',
  OPERATION_NOT_ALLOWED = 'OPERATION_NOT_ALLOWED'
}

AppError Base Class

export class AppError extends Error {
  public readonly code: ErrorCode;
  public readonly statusCode: number;
  public readonly details?: Record<string, any>;
  public readonly timestamp: string;

  constructor(
    code: ErrorCode,
    message: string,
    statusCode: number = 500,
    details?: Record<string, any>
  ) {
    super(message);
    this.name = 'AppError';
    this.code = code;
    this.statusCode = statusCode;
    this.details = details;
    this.timestamp = new Date().toISOString();
  }

  toJSON(): ApiError {
    return {
      success: false,
      error: {
        code: this.code,
        message: this.message,
        details: this.details,
        timestamp: this.timestamp
      }
    };
  }
}

Specialized Error Classes

// Validation errors
export class ValidationError extends AppError {
  constructor(message: string, details?: Record<string, any>) {
    super(ErrorCode.VALIDATION_ERROR, message, 400, details);
    this.name = 'ValidationError';
  }
}

// Not found errors
export class NotFoundError extends AppError {
  constructor(resource: string = 'Resource') {
    super(ErrorCode.NOT_FOUND, `${resource} not found`, 404);
    this.name = 'NotFoundError';
  }
}

// Authorization errors
export class UnauthorizedError extends AppError {
  constructor(message: string = 'Unauthorized access') {
    super(ErrorCode.UNAUTHORIZED, message, 401);
    this.name = 'UnauthorizedError';
  }
}

// Database errors
export class DatabaseError extends AppError {
  constructor(message: string = 'Database operation failed', details?: Record<string, any>) {
    super(ErrorCode.DATABASE_ERROR, message, 500, details);
    this.name = 'DatabaseError';
  }
}

API Error Handler

withErrorHandling Decorator

import { withErrorHandling, handleApiError } from '@/lib/errors/api-handler';

// Wrap API routes with error handling
export const GET = withErrorHandling(async (context) => {
  const { request, env } = context;
  
  // Your API logic here
  const data = await processRequest(request, env);
  
  return new Response(JSON.stringify({ success: true, data }), {
    status: 200,
    headers: { 'Content-Type': 'application/json' }
  });
});

Manual Error Handling

import { handleApiError, createApiError } from '@/lib/errors/api-handler';

export async function GET(request: Request) {
  try {
    // Validate request
    if (!request.headers.get('Authorization')) {
      throw new UnauthorizedError('Missing authorization header');
    }

    // Process request
    const data = await getData();
    
    return new Response(JSON.stringify({ success: true, data }), {
      status: 200,
      headers: { 'Content-Type': 'application/json' }
    });
  } catch (error) {
    return handleApiError(error, generateRequestId());
  }
}

Usage Examples

Validation Error Handling

import { ValidationError } from '@/lib/errors/types';

async function createPlayer(data: any) {
  // Validate required fields
  if (!data.email) {
    throw new ValidationError('Email is required', {
      field: 'email',
      value: data.email
    });
  }

  if (!data.username) {
    throw new ValidationError('Username is required', {
      field: 'username',
      value: data.username
    });
  }

  // Validate email format
  if (!isValidEmail(data.email)) {
    throw new ValidationError('Invalid email format', {
      field: 'email',
      value: data.email,
      expected: 'valid email address'
    });
  }

  // Process valid data
  return await playerService.create(data);
}

Database Error Handling

import { DatabaseError, NotFoundError } from '@/lib/errors/types';

async function getPlayerById(id: string) {
  try {
    const player = await db.prepare(`
      SELECT * FROM players WHERE uuid = ?
    `).bind(id).first();

    if (!player) {
      throw new NotFoundError('Player');
    }

    return player;
  } catch (error) {
    if (error instanceof NotFoundError) {
      throw error;
    }
    
    // Handle database-specific errors
    if (error.message.includes('database') || error.message.includes('SQLite')) {
      throw new DatabaseError('Failed to retrieve player', {
        playerId: id,
        originalError: error.message
      });
    }
    
    throw error;
  }
}

External Service Error Handling

import { AppError, ErrorCode } from '@/lib/errors/types';

async function callExternalAPI(url: string, data: any) {
  try {
    const response = await fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    });

    if (!response.ok) {
      if (response.status === 429) {
        throw new AppError(
          ErrorCode.API_RATE_LIMIT,
          'Rate limit exceeded',
          429,
          { retryAfter: response.headers.get('Retry-After') }
        );
      }
      
      if (response.status >= 500) {
        throw new AppError(
          ErrorCode.EXTERNAL_SERVICE_ERROR,
          'External service unavailable',
          502
        );
      }
      
      throw new AppError(
        ErrorCode.EXTERNAL_SERVICE_ERROR,
        `External service error: ${response.status}`,
        response.status
      );
    }

    return await response.json();
  } catch (error) {
    if (error instanceof AppError) {
      throw error;
    }
    
    // Handle network errors
    if (error.message.includes('fetch')) {
      throw new AppError(
        ErrorCode.EXTERNAL_SERVICE_ERROR,
        'Network error when calling external service',
        502,
        { originalError: error.message }
      );
    }
    
    throw error;
  }
}

Business Logic Error Handling

import { AppError, ErrorCode } from '@/lib/errors/types';

async function joinGame(playerId: string, gameId: string) {
  // Check if player is already in a game
  const currentGame = await getPlayerCurrentGame(playerId);
  if (currentGame) {
    throw new AppError(
      ErrorCode.BUSINESS_RULE_VIOLATION,
      'Player is already in a game',
      409,
      { currentGameId: currentGame.id }
    );
  }

  // Check if game is full
  const game = await getGame(gameId);
  if (!game) {
    throw new NotFoundError('Game');
  }

  if (game.playerCount >= game.maxPlayers) {
    throw new AppError(
      ErrorCode.BUSINESS_RULE_VIOLATION,
      'Game is full',
      409,
      { 
        gameId,
        currentPlayers: game.playerCount,
        maxPlayers: game.maxPlayers
      }
    );
  }

  // Check player level requirements
  const player = await getPlayer(playerId);
  if (player.level < game.minLevel) {
    throw new AppError(
      ErrorCode.INSUFFICIENT_PERMISSIONS,
      'Player level too low for this game',
      403,
      { 
        playerLevel: player.level,
        requiredLevel: game.minLevel
      }
    );
  }

  // Join the game
  return await addPlayerToGame(playerId, gameId);
}

Error Response Format

Standard API Error Response

interface ApiError {
  success: false;
  error: {
    code: string;
    message: string;
    details?: Record<string, any>;
    timestamp: string;
  };
}

// Example error response
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Email is required",
    "details": {
      "field": "email",
      "value": null
    },
    "timestamp": "2024-01-15T10:30:00.000Z"
  }
}

Success Response Format

interface ApiSuccess<T = any> {
  success: true;
  data: T;
  timestamp?: string;
}

// Example success response
{
  "success": true,
  "data": {
    "id": "123",
    "name": "Player Name",
    "level": 5
  },
  "timestamp": "2024-01-15T10:30:00.000Z"
}

Error Tracking and Monitoring

Request ID Generation

import { generateRequestId } from '@/lib/errors/api-handler';

export async function GET(request: Request) {
  const requestId = generateRequestId();
  
  try {
    // Add request ID to response headers
    const response = await processRequest(request);
    response.headers.set('X-Request-ID', requestId);
    return response;
  } catch (error) {
    return handleApiError(error, requestId);
  }
}

Error Logging Integration

import { apiLogger } from '@/lib/utils/logger';

export async function GET(request: Request) {
  const requestId = generateRequestId();
  
  try {
    return await processRequest(request);
  } catch (error) {
    // Log error with context
    apiLogger.error('API request failed', error, {
      requestId,
      endpoint: request.url,
      method: request.method,
      userAgent: request.headers.get('User-Agent')
    });
    
    return handleApiError(error, requestId);
  }
}

Best Practices

1. Use Specific Error Types

// Good: Specific error type
if (!player) {
  throw new NotFoundError('Player');
}

// Bad: Generic error
if (!player) {
  throw new Error('Player not found');
}

2. Provide Meaningful Context

// Good: Rich error context
throw new ValidationError('Invalid email format', {
  field: 'email',
  value: email,
  expected: 'valid email address',
  received: typeof email
});

// Bad: Minimal context
throw new ValidationError('Invalid email');

3. Handle Errors at Appropriate Levels

// Low-level: Database errors
async function getPlayerFromDB(id: string) {
  try {
    return await db.prepare('SELECT * FROM players WHERE id = ?').bind(id).first();
  } catch (error) {
    throw new DatabaseError('Failed to retrieve player', { playerId: id });
  }
}

// High-level: Business logic errors
async function getPlayer(id: string) {
  const player = await getPlayerFromDB(id);
  if (!player) {
    throw new NotFoundError('Player');
  }
  return player;
}

4. Consistent Error Handling

// Use the same error handling pattern across all routes
export const GET = withErrorHandling(async (context) => {
  // Route logic
});

export const POST = withErrorHandling(async (context) => {
  // Route logic
});

export const PUT = withErrorHandling(async (context) => {
  // Route logic
});

Testing

Error Testing

describe('Error Handling', () => {
  it('should return validation error for invalid input', async () => {
    const response = await app.request('/api/players', {
      method: 'POST',
      body: JSON.stringify({ email: 'invalid-email' })
    });

    expect(response.status).toBe(400);
    const data = await response.json();
    expect(data.success).toBe(false);
    expect(data.error.code).toBe('VALIDATION_ERROR');
  });

  it('should return not found error for missing resource', async () => {
    const response = await app.request('/api/players/nonexistent-id');
    
    expect(response.status).toBe(404);
    const data = await response.json();
    expect(data.success).toBe(false);
    expect(data.error.code).toBe('NOT_FOUND');
  });
});

Error Recovery Testing

describe('Error Recovery', () => {
  it('should handle database connection errors gracefully', async () => {
    // Mock database failure
    jest.spyOn(db, 'prepare').mockRejectedValue(new Error('Database connection failed'));
    
    const response = await app.request('/api/players');
    
    expect(response.status).toBe(500);
    const data = await response.json();
    expect(data.error.code).toBe('DATABASE_ERROR');
  });
});

This error handling system provides robust, consistent error management across the entire PadawanForge application.

PadawanForge v1.4.1