Error Handling System
Overview
PadawanForge implements a comprehensive error handling system that provides structured error management, standardized API responses, and robust error tracking. This system ensures consistent error handling across all application layers.
Architecture
Core Components
- Structured Error Types: Predefined error codes and categories
- API Error Standardization: Consistent error response format
- Error Categorization: Automatic error classification
- Request Tracking: End-to-end error tracing
- Debug Information: Development-friendly error details
Error Categories
- Authentication Errors: Unauthorized access, invalid tokens
- Validation Errors: Invalid input, missing required fields
- Resource Errors: Not found, conflicts, already exists
- Database Errors: Connection issues, query failures
- Server Errors: Internal errors, service unavailable
- External Service Errors: API failures, rate limits
- Business Logic Errors: Rule violations, insufficient permissions
Implementation
Error Types and Codes
import { ErrorCode, AppError, ValidationError, NotFoundError } from '@/lib/errors/types';
// Predefined error codes
enum ErrorCode {
UNAUTHORIZED = 'UNAUTHORIZED',
FORBIDDEN = 'FORBIDDEN',
INVALID_TOKEN = 'INVALID_TOKEN',
SESSION_EXPIRED = 'SESSION_EXPIRED',
VALIDATION_ERROR = 'VALIDATION_ERROR',
INVALID_INPUT = 'INVALID_INPUT',
MISSING_REQUIRED_FIELD = 'MISSING_REQUIRED_FIELD',
NOT_FOUND = 'NOT_FOUND',
ALREADY_EXISTS = 'ALREADY_EXISTS',
RESOURCE_CONFLICT = 'RESOURCE_CONFLICT',
DATABASE_ERROR = 'DATABASE_ERROR',
CONNECTION_ERROR = 'CONNECTION_ERROR',
TRANSACTION_FAILED = 'TRANSACTION_FAILED',
INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR',
SERVICE_UNAVAILABLE = 'SERVICE_UNAVAILABLE',
TIMEOUT = 'TIMEOUT',
EXTERNAL_SERVICE_ERROR = 'EXTERNAL_SERVICE_ERROR',
API_RATE_LIMIT = 'API_RATE_LIMIT',
BUSINESS_RULE_VIOLATION = 'BUSINESS_RULE_VIOLATION',
INSUFFICIENT_PERMISSIONS = 'INSUFFICIENT_PERMISSIONS',
OPERATION_NOT_ALLOWED = 'OPERATION_NOT_ALLOWED'
}
AppError Base Class
export class AppError extends Error {
public readonly code: ErrorCode;
public readonly statusCode: number;
public readonly details?: Record<string, any>;
public readonly timestamp: string;
constructor(
code: ErrorCode,
message: string,
statusCode: number = 500,
details?: Record<string, any>
) {
super(message);
this.name = 'AppError';
this.code = code;
this.statusCode = statusCode;
this.details = details;
this.timestamp = new Date().toISOString();
}
toJSON(): ApiError {
return {
success: false,
error: {
code: this.code,
message: this.message,
details: this.details,
timestamp: this.timestamp
}
};
}
}
Specialized Error Classes
// Validation errors
export class ValidationError extends AppError {
constructor(message: string, details?: Record<string, any>) {
super(ErrorCode.VALIDATION_ERROR, message, 400, details);
this.name = 'ValidationError';
}
}
// Not found errors
export class NotFoundError extends AppError {
constructor(resource: string = 'Resource') {
super(ErrorCode.NOT_FOUND, `${resource} not found`, 404);
this.name = 'NotFoundError';
}
}
// Authorization errors
export class UnauthorizedError extends AppError {
constructor(message: string = 'Unauthorized access') {
super(ErrorCode.UNAUTHORIZED, message, 401);
this.name = 'UnauthorizedError';
}
}
// Database errors
export class DatabaseError extends AppError {
constructor(message: string = 'Database operation failed', details?: Record<string, any>) {
super(ErrorCode.DATABASE_ERROR, message, 500, details);
this.name = 'DatabaseError';
}
}
API Error Handler
withErrorHandling Decorator
import { withErrorHandling, handleApiError } from '@/lib/errors/api-handler';
// Wrap API routes with error handling
export const GET = withErrorHandling(async (context) => {
const { request, env } = context;
// Your API logic here
const data = await processRequest(request, env);
return new Response(JSON.stringify({ success: true, data }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
});
Manual Error Handling
import { handleApiError, createApiError } from '@/lib/errors/api-handler';
export async function GET(request: Request) {
try {
// Validate request
if (!request.headers.get('Authorization')) {
throw new UnauthorizedError('Missing authorization header');
}
// Process request
const data = await getData();
return new Response(JSON.stringify({ success: true, data }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
} catch (error) {
return handleApiError(error, generateRequestId());
}
}
Usage Examples
Validation Error Handling
import { ValidationError } from '@/lib/errors/types';
async function createPlayer(data: any) {
// Validate required fields
if (!data.email) {
throw new ValidationError('Email is required', {
field: 'email',
value: data.email
});
}
if (!data.username) {
throw new ValidationError('Username is required', {
field: 'username',
value: data.username
});
}
// Validate email format
if (!isValidEmail(data.email)) {
throw new ValidationError('Invalid email format', {
field: 'email',
value: data.email,
expected: 'valid email address'
});
}
// Process valid data
return await playerService.create(data);
}
Database Error Handling
import { DatabaseError, NotFoundError } from '@/lib/errors/types';
async function getPlayerById(id: string) {
try {
const player = await db.prepare(`
SELECT * FROM players WHERE uuid = ?
`).bind(id).first();
if (!player) {
throw new NotFoundError('Player');
}
return player;
} catch (error) {
if (error instanceof NotFoundError) {
throw error;
}
// Handle database-specific errors
if (error.message.includes('database') || error.message.includes('SQLite')) {
throw new DatabaseError('Failed to retrieve player', {
playerId: id,
originalError: error.message
});
}
throw error;
}
}
External Service Error Handling
import { AppError, ErrorCode } from '@/lib/errors/types';
async function callExternalAPI(url: string, data: any) {
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) {
if (response.status === 429) {
throw new AppError(
ErrorCode.API_RATE_LIMIT,
'Rate limit exceeded',
429,
{ retryAfter: response.headers.get('Retry-After') }
);
}
if (response.status >= 500) {
throw new AppError(
ErrorCode.EXTERNAL_SERVICE_ERROR,
'External service unavailable',
502
);
}
throw new AppError(
ErrorCode.EXTERNAL_SERVICE_ERROR,
`External service error: ${response.status}`,
response.status
);
}
return await response.json();
} catch (error) {
if (error instanceof AppError) {
throw error;
}
// Handle network errors
if (error.message.includes('fetch')) {
throw new AppError(
ErrorCode.EXTERNAL_SERVICE_ERROR,
'Network error when calling external service',
502,
{ originalError: error.message }
);
}
throw error;
}
}
Business Logic Error Handling
import { AppError, ErrorCode } from '@/lib/errors/types';
async function joinGame(playerId: string, gameId: string) {
// Check if player is already in a game
const currentGame = await getPlayerCurrentGame(playerId);
if (currentGame) {
throw new AppError(
ErrorCode.BUSINESS_RULE_VIOLATION,
'Player is already in a game',
409,
{ currentGameId: currentGame.id }
);
}
// Check if game is full
const game = await getGame(gameId);
if (!game) {
throw new NotFoundError('Game');
}
if (game.playerCount >= game.maxPlayers) {
throw new AppError(
ErrorCode.BUSINESS_RULE_VIOLATION,
'Game is full',
409,
{
gameId,
currentPlayers: game.playerCount,
maxPlayers: game.maxPlayers
}
);
}
// Check player level requirements
const player = await getPlayer(playerId);
if (player.level < game.minLevel) {
throw new AppError(
ErrorCode.INSUFFICIENT_PERMISSIONS,
'Player level too low for this game',
403,
{
playerLevel: player.level,
requiredLevel: game.minLevel
}
);
}
// Join the game
return await addPlayerToGame(playerId, gameId);
}
Error Response Format
Standard API Error Response
interface ApiError {
success: false;
error: {
code: string;
message: string;
details?: Record<string, any>;
timestamp: string;
};
}
// Example error response
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Email is required",
"details": {
"field": "email",
"value": null
},
"timestamp": "2024-01-15T10:30:00.000Z"
}
}
Success Response Format
interface ApiSuccess<T = any> {
success: true;
data: T;
timestamp?: string;
}
// Example success response
{
"success": true,
"data": {
"id": "123",
"name": "Player Name",
"level": 5
},
"timestamp": "2024-01-15T10:30:00.000Z"
}
Error Tracking and Monitoring
Request ID Generation
import { generateRequestId } from '@/lib/errors/api-handler';
export async function GET(request: Request) {
const requestId = generateRequestId();
try {
// Add request ID to response headers
const response = await processRequest(request);
response.headers.set('X-Request-ID', requestId);
return response;
} catch (error) {
return handleApiError(error, requestId);
}
}
Error Logging Integration
import { apiLogger } from '@/lib/utils/logger';
export async function GET(request: Request) {
const requestId = generateRequestId();
try {
return await processRequest(request);
} catch (error) {
// Log error with context
apiLogger.error('API request failed', error, {
requestId,
endpoint: request.url,
method: request.method,
userAgent: request.headers.get('User-Agent')
});
return handleApiError(error, requestId);
}
}
Best Practices
1. Use Specific Error Types
// Good: Specific error type
if (!player) {
throw new NotFoundError('Player');
}
// Bad: Generic error
if (!player) {
throw new Error('Player not found');
}
2. Provide Meaningful Context
// Good: Rich error context
throw new ValidationError('Invalid email format', {
field: 'email',
value: email,
expected: 'valid email address',
received: typeof email
});
// Bad: Minimal context
throw new ValidationError('Invalid email');
3. Handle Errors at Appropriate Levels
// Low-level: Database errors
async function getPlayerFromDB(id: string) {
try {
return await db.prepare('SELECT * FROM players WHERE id = ?').bind(id).first();
} catch (error) {
throw new DatabaseError('Failed to retrieve player', { playerId: id });
}
}
// High-level: Business logic errors
async function getPlayer(id: string) {
const player = await getPlayerFromDB(id);
if (!player) {
throw new NotFoundError('Player');
}
return player;
}
4. Consistent Error Handling
// Use the same error handling pattern across all routes
export const GET = withErrorHandling(async (context) => {
// Route logic
});
export const POST = withErrorHandling(async (context) => {
// Route logic
});
export const PUT = withErrorHandling(async (context) => {
// Route logic
});
Testing
Error Testing
describe('Error Handling', () => {
it('should return validation error for invalid input', async () => {
const response = await app.request('/api/players', {
method: 'POST',
body: JSON.stringify({ email: 'invalid-email' })
});
expect(response.status).toBe(400);
const data = await response.json();
expect(data.success).toBe(false);
expect(data.error.code).toBe('VALIDATION_ERROR');
});
it('should return not found error for missing resource', async () => {
const response = await app.request('/api/players/nonexistent-id');
expect(response.status).toBe(404);
const data = await response.json();
expect(data.success).toBe(false);
expect(data.error.code).toBe('NOT_FOUND');
});
});
Error Recovery Testing
describe('Error Recovery', () => {
it('should handle database connection errors gracefully', async () => {
// Mock database failure
jest.spyOn(db, 'prepare').mockRejectedValue(new Error('Database connection failed'));
const response = await app.request('/api/players');
expect(response.status).toBe(500);
const data = await response.json();
expect(data.error.code).toBe('DATABASE_ERROR');
});
});
This error handling system provides robust, consistent error management across the entire PadawanForge application.