Files
pos-system/.agent/rules/error-handling-patterns.md

10 KiB

trigger
trigger
always_on

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 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.

Quick Reference

Error Class Status When to Use
NotFoundError 404 Resource not found
BadRequestError 400 Invalid request
ValidationError 422 Input validation failed
UnauthorizedError 401 Auth required
ForbiddenError 403 Access denied
ConflictError 409 Duplicate/conflict
RateLimitError 429 Too many requests
InternalServerError 500 Server error

Error Response Format:

{
  success: false,
  error: {
    code: "RESOURCE_001",
    message: "User not found",
    details?: { /* validation details */ }
  },
  timestamp: "2024-01-01T00:00:00.000Z"
}

Common Error Codes:

Code Category Description
AUTH_001 Auth Unauthorized
AUTH_003 Auth Invalid token
VALIDATION_001 Validation Validation failed
RESOURCE_001 Resource Not found
RESOURCE_002 Resource Already exists
DB_001 Database Database error
SYS_001 System Internal error

Essential Imports:

import { NotFoundError, ValidationError, ConflictError } from '../errors/http-error';
import { ErrorCode } from '../errors/error-codes';
import { asyncHandler } from '../middlewares/async.middleware';

Resources