Files
pos-system/docs/en/skills/error-handling-patterns.md
Ho Ngoc Hai 2640b351c3 Enhance documentation with detailed diagrams and structured flows
- Added request/response flow diagrams to api-design and api-gateway-advanced skills for better visualization of processes.
- Introduced configuration loading flow in configuration-management skill to clarify the configuration process.
- Included error propagation flow in error-handling-patterns skill to illustrate error handling across layers.
- Enhanced various skills with additional diagrams to improve understanding of complex concepts.

These updates aim to provide clearer guidance and improve the overall documentation experience for developers.
2026-01-01 23:22:54 +07:00

13 KiB

name, description
name description
error-handling-patterns Error handling patterns and conventions for GoodGo microservices. Use when implementing error handling, creating custom error classes, handling exceptions, standardizing error responses, or debugging error scenarios.

Error Handling Patterns

When to Use This Skill

Use this skill when:

  • Implementing error handling in services, controllers, or repositories
  • Creating custom error classes for specific error scenarios
  • Standardizing error responses across APIs
  • Handling exceptions from external services or database operations
  • Implementing error middleware and global error handlers
  • Debugging error scenarios and improving error messages
  • Distinguishing between operational and programming errors

Core Concepts

Error Types

  1. Operational Errors: Expected errors that occur during normal operation

    • Examples: Validation errors, authentication failures, resource not found
    • Should be handled gracefully and return appropriate HTTP status codes
    • Safe to expose error details to clients (with caution)
  2. Programming Errors: Unexpected errors due to bugs in code

    • Examples: Null pointer exceptions, type errors, logic bugs
    • Should be logged with full details for debugging
    • Should return generic error messages to clients (hide implementation details)

Error Propagation Flow

The following diagram illustrates how errors propagate through the application layers:

flowchart TD
    A[Request] --> B[Controller]
    B --> C[Service Layer]
    C --> D[Repository Layer]
    D --> E{Error Occurs?}
    E -->|Yes| F[Throw HttpError]
    E -->|No| G[Return Data]
    F --> H[Error Middleware]
    H --> I{Error Type?}
    I -->|HttpError| J[Extract Status/Code]
    I -->|Prisma Error| K[Map to HttpError]
    I -->|Zod Error| L[Map to ValidationError]
    I -->|Unknown| M[Map to InternalServerError]
    J --> N[Log Error]
    K --> N
    L --> N
    M --> N
    N --> O{Is Operational?}
    O -->|Yes| P[Log as Warning]
    O -->|No| Q[Log as Error]
    P --> R[Format Response]
    Q --> R
    R --> S{Is Production?}
    S -->|Yes & 5xx| T[Generic Message]
    S -->|No or < 5xx| U[Detailed Message]
    T --> V[Send Response]
    U --> V
    G --> V

Error Hierarchy Structure

The error class hierarchy shows the relationship between different error types:

classDiagram
    class Error {
        <<built-in>>
        +message: string
        +stack: string
    }
    class HttpError {
        +statusCode: number
        +errorCode: string
        +isOperational: boolean
        +details?: any
        +toApiResponse()
    }
    class NotFoundError {
        +statusCode: 404
    }
    class BadRequestError {
        +statusCode: 400
    }
    class ValidationError {
        +statusCode: 422
    }
    class UnauthorizedError {
        +statusCode: 401
    }
    class ForbiddenError {
        +statusCode: 403
    }
    class ConflictError {
        +statusCode: 409
    }
    class RateLimitError {
        +statusCode: 429
    }
    class InternalServerError {
        +statusCode: 500
    }
    class ServiceUnavailableError {
        +statusCode: 503
    }
    class DatabaseError {
        +statusCode: 500
    }
    class ExternalServiceError {
        +statusCode: 502
    }
    
    Error <|-- HttpError
    HttpError <|-- NotFoundError
    HttpError <|-- BadRequestError
    HttpError <|-- ValidationError
    HttpError <|-- UnauthorizedError
    HttpError <|-- ForbiddenError
    HttpError <|-- ConflictError
    HttpError <|-- RateLimitError
    HttpError <|-- InternalServerError
    HttpError <|-- ServiceUnavailableError
    HttpError <|-- DatabaseError
    HttpError <|-- ExternalServiceError

Error Handling Decision Tree

Use this decision tree to determine which error class to use:

flowchart TD
    A[Error Occurs] --> B{Error Type?}
    B -->|Resource Not Found| C[NotFoundError<br/>404]
    B -->|Invalid Input| D{Validation?}
    B -->|Authentication| E{Type?}
    B -->|Resource Conflict| F[ConflictError<br/>409]
    B -->|Rate Limit| G[RateLimitError<br/>429]
    B -->|Database| H[DatabaseError<br/>500]
    B -->|External Service| I[ExternalServiceError<br/>502]
    B -->|Service Unavailable| J[ServiceUnavailableError<br/>503]
    B -->|Unknown/Programming| K[InternalServerError<br/>500]
    D -->|Schema Validation| L[ValidationError<br/>422]
    D -->|Bad Request Format| M[BadRequestError<br/>400]
    E -->|No Token/Invalid| N[UnauthorizedError<br/>401]
    E -->|No Permission| O[ForbiddenError<br/>403]
    C --> P[Set isOperational: true]
    L --> P
    M --> P
    N --> P
    O --> P
    F --> P
    G --> P
    H --> Q{Is Operational?}
    I --> Q
    J --> Q
    K --> R[Set isOperational: false]
    Q -->|Yes| P
    Q -->|No| R
    P --> S[Include Error Code]
    R --> S
    S --> T[Add Context Details]
    T --> U[Throw Error]

Error Code System

The platform uses a centralized error code system (ErrorCode enum) that:

  • Provides unique identifiers for each error type
  • Maps to HTTP status codes consistently
  • Enables error tracking and analytics
  • Supports internationalization

Error codes follow the pattern: {CATEGORY}_{NUMBER}

  • AUTH_001 - Authentication errors
  • VALIDATION_001 - Validation errors
  • RESOURCE_001 - Resource errors
  • DB_001 - Database errors

Patterns

Base Error Class: HttpError

All custom errors extend the HttpError base class:

export class HttpError extends Error {
  public readonly statusCode: number;
  public readonly errorCode: string;
  public readonly isOperational: boolean;
  public readonly details?: any;

  constructor(
    message: string,
    statusCode: number = 500,
    errorCode: string = 'INTERNAL_ERROR',
    isOperational: boolean = true,
    details?: any
  ) {
    super(message);
    this.statusCode = statusCode;
    this.errorCode = errorCode;
    this.isOperational = isOperational;
    this.details = details;
    Error.captureStackTrace(this, this.constructor);
  }

  toApiResponse() {
    return {
      success: false,
      error: {
        code: this.errorCode,
        message: this.message,
        ...(this.details && { details: this.details }),
      },
      timestamp: new Date().toISOString(),
    };
  }
}

Standard Error Classes

Use these predefined error classes for common scenarios:

Resource Errors:

  • NotFoundError - 404: Resource not found
  • ConflictError - 409: Resource conflict (e.g., duplicate)

Validation Errors:

  • ValidationError - 422: Input validation failed
  • BadRequestError - 400: Invalid request

Authentication/Authorization:

  • UnauthorizedError - 401: Authentication required
  • ForbiddenError - 403: Access denied

System Errors:

  • InternalServerError - 500: Internal server error (programming error)
  • ServiceUnavailableError - 503: Service temporarily unavailable
  • DatabaseError - 500: Database operation failed
  • ExternalServiceError - 502: External service error

Rate Limiting:

  • RateLimitError - 429: Too many requests

Error Code Enum

Centralized error codes in ErrorCode enum:

export enum ErrorCode {
  // Authentication & Authorization
  UNAUTHORIZED = 'AUTH_001',
  FORBIDDEN = 'AUTH_002',
  INVALID_TOKEN = 'AUTH_003',
  TOKEN_EXPIRED = 'AUTH_004',
  
  // Validation
  VALIDATION_ERROR = 'VALIDATION_001',
  INVALID_FORMAT = 'VALIDATION_002',
  
  // Resources
  NOT_FOUND = 'RESOURCE_001',
  ALREADY_EXISTS = 'RESOURCE_002',
  CONFLICT = 'RESOURCE_003',
  
  // Database
  DATABASE_ERROR = 'DB_001',
  CONSTRAINT_VIOLATION = 'DB_004',
  
  // System
  INTERNAL_ERROR = 'SYS_001',
  RATE_LIMIT_EXCEEDED = 'SYS_003',
}

Using Errors in Services

import { NotFoundError, ConflictError } from '../errors/http-error';
import { ErrorCode } from '../errors/error-codes';

export class UserService {
  async getUserById(id: string) {
    const user = await this.repository.findById(id);
    
    if (!user) {
      throw new NotFoundError('User', { id });
    }
    
    return user;
  }
  
  async createUser(data: CreateUserInput) {
    const existing = await this.repository.findByEmail(data.email);
    
    if (existing) {
      throw new ConflictError('User with this email already exists');
    }
    
    return await this.repository.create(data);
  }
}

Error Middleware Pattern

Global error handler middleware processes all errors:

export const errorHandler = (
  err: any,
  req: express.Request,
  res: express.Response,
  _next: express.NextFunction
): void => {
  let statusCode = 500;
  let errorCode = ErrorCode.INTERNAL_ERROR;
  let message = 'Internal server error';
  let isOperational = false;
  
  // Handle HttpError instances
  if (err instanceof HttpError) {
    statusCode = err.statusCode;
    errorCode = err.errorCode as ErrorCode;
    message = err.message;
    isOperational = err.isOperational;
  }
  // Handle Prisma errors
  else if (err.code === 'P2002') {
    statusCode = 409;
    errorCode = ErrorCode.CONSTRAINT_VIOLATION;
    message = 'Resource already exists';
    isOperational = true;
  }
  // Handle Zod validation errors
  else if (err.name === 'ZodError') {
    statusCode = 422;
    errorCode = ErrorCode.VALIDATION_ERROR;
    message = 'Validation failed';
    // Extract validation details
  }
  
  // Log error
  if (!isOperational || statusCode >= 500) {
    logger.error('Unhandled error', { error: err, statusCode, errorCode });
  } else {
    logger.warn('Operational error', { error: err, statusCode, errorCode });
  }
  
  // Send response
  const response = {
    success: false,
    error: {
      code: errorCode,
      message: isProduction && statusCode >= 500 
        ? 'Internal server error' 
        : message,
    },
    timestamp: new Date().toISOString(),
  };
  
  res.status(statusCode).json(response);
};

Async Error Wrapper

Wrap async route handlers to catch promise rejections:

export const asyncHandler = (fn: Function) => {
  return (req: express.Request, res: express.Response, next: express.NextFunction) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
};

// Usage
router.get('/users/:id', asyncHandler(async (req, res) => {
  const user = await userService.getUserById(req.params.id);
  res.json({ success: true, data: user });
}));

Error Response Format

Standardized error response format:

{
  success: false,
  error: {
    code: "RESOURCE_001",
    message: "User not found",
    details?: {
      // Optional additional details (not in production for 5xx errors)
    }
  },
  timestamp: "2024-01-01T00:00:00.000Z"
}

Best Practices

  1. Use Specific Error Classes: Use the most specific error class available
  2. Include Context: Provide helpful error messages with context
  3. Mark Operational Errors: Set isOperational: true for expected errors
  4. Don't Expose Internal Details: Hide implementation details in production
  5. Log Appropriately: Use logger.error() for programming errors, logger.warn() for operational errors
  6. Handle Database Errors: Map Prisma errors to appropriate HTTP errors
  7. Use Error Codes: Always use ErrorCode enum for consistency
  8. Validate Early: Validate input early to catch errors before processing

Common Mistakes

  1. Not Using Error Classes: Using generic Error instead of specific error classes
  2. Exposing Stack Traces: Including stack traces in production responses
  3. Ignoring Errors: Not handling errors in async operations
  4. Generic Error Messages: Using vague error messages without context
  5. Not Logging: Forgetting to log errors for debugging
  6. Wrong HTTP Status Codes: Using incorrect status codes for error types
  7. Not Using Error Middleware: Handling errors manually instead of using middleware

Troubleshooting

Error Not Caught by Middleware

Problem: Error not being caught by error middleware Solution: Ensure error middleware is added last, after all routes. Use asyncHandler for async route handlers.

Generic Error Messages in Production

Problem: Generic "Internal server error" shown even for operational errors Solution: Check isOperational flag is set correctly. Verify error middleware handles all error types.

Error Code Not Found

Problem: Error code not in ErrorCode enum Solution: Add error code to enum following naming convention. Update ERROR_CODE_TO_STATUS mapping.

Stack Traces Exposed

Problem: Stack traces visible in API responses Solution: Ensure production environment checks are in place. Use error middleware to filter stack traces.

Resources