- 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.
461 lines
13 KiB
Markdown
461 lines
13 KiB
Markdown
---
|
|
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 {
|
|
<<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:
|
|
|
|
```mermaid
|
|
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:
|
|
|
|
```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
|