- 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.
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
-
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)
-
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 errorsVALIDATION_001- Validation errorsRESOURCE_001- Resource errorsDB_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 foundConflictError- 409: Resource conflict (e.g., duplicate)
Validation Errors:
ValidationError- 422: Input validation failedBadRequestError- 400: Invalid request
Authentication/Authorization:
UnauthorizedError- 401: Authentication requiredForbiddenError- 403: Access denied
System Errors:
InternalServerError- 500: Internal server error (programming error)ServiceUnavailableError- 503: Service temporarily unavailableDatabaseError- 500: Database operation failedExternalServiceError- 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
- Use Specific Error Classes: Use the most specific error class available
- Include Context: Provide helpful error messages with context
- Mark Operational Errors: Set
isOperational: truefor expected errors - Don't Expose Internal Details: Hide implementation details in production
- Log Appropriately: Use
logger.error()for programming errors,logger.warn()for operational errors - Handle Database Errors: Map Prisma errors to appropriate HTTP errors
- Use Error Codes: Always use
ErrorCodeenum for consistency - Validate Early: Validate input early to catch errors before processing
Common Mistakes
- Not Using Error Classes: Using generic
Errorinstead of specific error classes - Exposing Stack Traces: Including stack traces in production responses
- Ignoring Errors: Not handling errors in async operations
- Generic Error Messages: Using vague error messages without context
- Not Logging: Forgetting to log errors for debugging
- Wrong HTTP Status Codes: Using incorrect status codes for error types
- 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 - Base error classes
- Error Codes - Error code definitions
- Error Middleware - Global error handler
- API Design - API response formats
- Security - Security error handling