import { PlatformError } from './base.errors.js'; import type { IErrorContext } from './base.errors.js'; import { ErrorCategory, ErrorRecoverability, ErrorSeverity } from './error.codes.js'; import { logger } from '../logger.js'; /** * Error handler configuration */ export interface IErrorHandlerConfig { /** Whether to log errors automatically */ logErrors: boolean; /** Whether to include stack traces in prod environment */ includeStacksInProd: boolean; /** Default retry options */ retry: { /** Maximum retry attempts */ maxAttempts: number; /** Base delay between retries in ms */ baseDelay: number; /** Maximum delay between retries in ms */ maxDelay: number; /** Backoff factor for exponential backoff */ backoffFactor: number; }; } /** * Global error handler configuration */ const config: IErrorHandlerConfig = { logErrors: true, includeStacksInProd: false, retry: { maxAttempts: 3, baseDelay: 1000, maxDelay: 30000, backoffFactor: 2 } }; /** * Error handler utility * Provides methods for consistent error handling across the platform */ export class ErrorHandler { /** * Current configuration */ public static config = config; /** * Update error handler configuration * * @param newConfig New configuration (partial) */ public static configure(newConfig: Partial): void { ErrorHandler.config = { ...ErrorHandler.config, ...newConfig, retry: { ...ErrorHandler.config.retry, ...(newConfig.retry || {}) } }; } /** * Convert any error to a PlatformError * * @param error Error to convert * @param defaultCode Default error code if not a PlatformError * @param context Additional context * @returns PlatformError instance */ public static toPlatformError( error: any, defaultCode: string, context: IErrorContext = {} ): PlatformError { // If already a PlatformError, just add context if (error instanceof PlatformError) { // Add context if provided if (Object.keys(context).length > 0) { return new (error.constructor as typeof PlatformError)( error.message, error.code, error.severity, error.category, error.recoverability, { ...error.context, ...context, data: { ...(error.context.data || {}), ...(context.data || {}) } } ); } return error; } // Convert standard Error to PlatformError if (error instanceof Error) { return new PlatformError( error.message, defaultCode, ErrorSeverity.MEDIUM, ErrorCategory.OPERATION, ErrorRecoverability.NON_RECOVERABLE, { ...context, data: { ...(context.data || {}), originalError: { name: error.name, message: error.message, stack: error.stack } } } ); } // Not an Error instance return new PlatformError( typeof error === 'string' ? error : 'Unknown error', defaultCode, ErrorSeverity.MEDIUM, ErrorCategory.OPERATION, ErrorRecoverability.NON_RECOVERABLE, context ); } /** * Format an error for API responses * Sanitizes errors for safe external exposure * * @param error Error to format * @param includeDetails Whether to include detailed information * @returns Formatted error object */ public static formatErrorForResponse( error: any, includeDetails: boolean = false ): Record { const platformError = ErrorHandler.toPlatformError( error, 'PLATFORM_OPERATION_ERROR' ); // Basic error information const responseError: Record = { code: platformError.code, message: platformError.getUserMessage(), requestId: platformError.context.requestId }; // Include more details if requested if (includeDetails) { responseError.details = { severity: platformError.severity, category: platformError.category, rawMessage: platformError.message, data: platformError.context.data }; // Only include stack trace in non-production or if explicitly enabled if (process.env.NODE_ENV !== 'production' || ErrorHandler.config.includeStacksInProd) { responseError.details.stack = platformError.stack; } } return responseError; } /** * Handle an error with consistent logging and formatting * * @param error Error to handle * @param defaultCode Default error code if not a PlatformError * @param context Additional context * @returns Formatted error for response */ public static handleError( error: any, defaultCode: string, context: IErrorContext = {} ): Record { const platformError = ErrorHandler.toPlatformError( error, defaultCode, context ); // Log the error if enabled if (ErrorHandler.config.logErrors) { logger.error(platformError.message, { error_code: platformError.code, error_name: platformError.name, error_severity: platformError.severity, error_category: platformError.category, error_recoverability: platformError.recoverability, ...platformError.context, stack: platformError.stack }); } // Return formatted error for response const isDetailedMode = process.env.NODE_ENV !== 'production'; return ErrorHandler.formatErrorForResponse(platformError, isDetailedMode); } /** * Execute a function with error handling * * @param fn Function to execute * @param defaultCode Default error code if the function throws * @param context Additional context * @returns Function result or error */ public static async execute( fn: () => Promise, defaultCode: string, context: IErrorContext = {} ): Promise { try { return await fn(); } catch (error) { throw ErrorHandler.toPlatformError(error, defaultCode, context); } } /** * Execute a function with retries and exponential backoff * * @param fn Function to execute * @param defaultCode Default error code if the function throws * @param options Retry options * @param context Additional context * @returns Function result or error after max retries */ public static async executeWithRetry( fn: () => Promise, defaultCode: string, options: { maxAttempts?: number; baseDelay?: number; maxDelay?: number; backoffFactor?: number; retryableErrorCodes?: string[]; retryableErrorPatterns?: RegExp[]; onRetry?: (error: PlatformError, attempt: number, delay: number) => void; } = {}, context: IErrorContext = {} ): Promise { const { maxAttempts = ErrorHandler.config.retry.maxAttempts, baseDelay = ErrorHandler.config.retry.baseDelay, maxDelay = ErrorHandler.config.retry.maxDelay, backoffFactor = ErrorHandler.config.retry.backoffFactor, retryableErrorCodes = [], retryableErrorPatterns = [], onRetry = () => {} } = options; let lastError: PlatformError; for (let attempt = 0; attempt < maxAttempts; attempt++) { try { return await fn(); } catch (error) { // Convert to PlatformError const platformError = ErrorHandler.toPlatformError( error, defaultCode, { ...context, retry: { currentRetry: attempt, maxRetries: maxAttempts, nextRetryAt: 0 // Will be set below if retrying } } ); lastError = platformError; // Check if we should retry const isLastAttempt = attempt >= maxAttempts - 1; if (isLastAttempt) { // No more retries throw platformError; } // Check if error is retryable const isRetryable = // Built-in recoverability platformError.recoverability === ErrorRecoverability.RECOVERABLE || platformError.recoverability === ErrorRecoverability.MAYBE_RECOVERABLE || platformError.recoverability === ErrorRecoverability.TRANSIENT || // Specifically included error codes retryableErrorCodes.includes(platformError.code) || // Matches error message patterns retryableErrorPatterns.some(pattern => pattern.test(platformError.message)); if (!isRetryable) { throw platformError; } // Calculate delay with exponential backoff const delay = Math.min(baseDelay * Math.pow(backoffFactor, attempt), maxDelay); // Add jitter to prevent thundering herd problem (±20%) const jitter = 0.8 + Math.random() * 0.4; const actualDelay = Math.floor(delay * jitter); // Update nextRetryAt in error context const nextRetryAt = Date.now() + actualDelay; platformError.context.retry!.nextRetryAt = nextRetryAt; // Log retry attempt logger.warn(`Retrying operation after error (attempt ${attempt + 1}/${maxAttempts}): ${platformError.message}`, { error_code: platformError.code, retry_attempt: attempt + 1, retry_max_attempts: maxAttempts, retry_delay_ms: actualDelay, retry_next_at: new Date(nextRetryAt).toISOString() }); // Call onRetry callback onRetry(platformError, attempt + 1, actualDelay); // Wait before next retry await new Promise(resolve => setTimeout(resolve, actualDelay)); } } // This should never happen, but TypeScript needs it throw lastError!; } } /** * Create a middleware for handling errors in HTTP requests * * @returns Middleware function */ export function createErrorHandlerMiddleware() { return (error: any, req: any, res: any, next: any) => { // Add request context const context: IErrorContext = { requestId: req.headers['x-request-id'] || req.headers['x-correlation-id'], component: 'HttpServer', operation: `${req.method} ${req.url}`, data: { method: req.method, url: req.url, query: req.query, params: req.params, ip: req.ip || req.connection.remoteAddress, userAgent: req.headers['user-agent'] } }; // Handle the error const formattedError = ErrorHandler.handleError( error, 'PLATFORM_OPERATION_ERROR', context ); // Set status code based on error type let statusCode = 500; if (error instanceof PlatformError) { // Map error categories to HTTP status codes switch (error.category) { case ErrorCategory.VALIDATION: statusCode = 400; break; case ErrorCategory.AUTHENTICATION: statusCode = 401; break; case ErrorCategory.RESOURCE: statusCode = 429; break; case ErrorCategory.OPERATION: statusCode = 400; break; default: statusCode = 500; } } else if (error.statusCode) { // Use provided status code if available statusCode = error.statusCode; } // Send error response res.status(statusCode).json({ success: false, error: formattedError }); }; }