- Updated skill documentation files to include structured metadata for better organization. - Enhanced bilingual descriptions and guidelines for clarity in both English and Vietnamese. - Refined sections on usage, best practices, and related skills to ensure consistency across all documentation. - Improved formatting and removed outdated references to streamline the documentation experience. - Added best practices checklists to relevant skills for better usability and adherence to standards.
321 lines
9.4 KiB
Markdown
321 lines
9.4 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 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
|