--- name: error-handling-patterns description: 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: ```mermaid 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: ```mermaid classDiagram class Error { <> +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: ```mermaid flowchart TD A[Error Occurs] --> B{Error Type?} B -->|Resource Not Found| C[NotFoundError
404] B -->|Invalid Input| D{Validation?} B -->|Authentication| E{Type?} B -->|Resource Conflict| F[ConflictError
409] B -->|Rate Limit| G[RateLimitError
429] B -->|Database| H[DatabaseError
500] B -->|External Service| I[ExternalServiceError
502] B -->|Service Unavailable| J[ServiceUnavailableError
503] B -->|Unknown/Programming| K[InternalServerError
500] D -->|Schema Validation| L[ValidationError
422] D -->|Bad Request Format| M[BadRequestError
400] E -->|No Token/Invalid| N[UnauthorizedError
401] E -->|No Permission| O[ForbiddenError
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: ```typescript 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: ```typescript 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 ```typescript 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: ```typescript 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: ```typescript 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: ```typescript { 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 - [Error Classes](../../services/iam-service/src/errors/http-error.ts) - Base error classes - [Error Codes](../../services/iam-service/src/errors/error-codes.ts) - Error code definitions - [Error Middleware](../../services/iam-service/src/middlewares/error.middleware.ts) - Global error handler - [API Design](../api-design/SKILL.md) - API response formats - [Security](../security/SKILL.md) - Security error handling