import { Request, Response, NextFunction } from "express" import * as Sentry from "@sentry/node" import { logger, LogContext } from "../utils/logger.js" // Simple error handling without categories // Simple error classes export class ApiError extends Error { public readonly statusCode: number public readonly isOperational: boolean public readonly context?: Record constructor( message: string, statusCode: number = 500, isOperational: boolean = true, context?: Record, ) { super(message) this.name = this.constructor.name this.statusCode = statusCode this.isOperational = isOperational this.context = context // Maintains proper stack trace for where our error was thrown Error.captureStackTrace(this, this.constructor) } } // Validation errors export class ValidationError extends ApiError { constructor(message: string, context?: Record) { super(message, 400, true, context) } } // Authentication errors export class AuthenticationError extends ApiError { constructor(message: string = "Authentication failed", context?: Record) { super(message, 401, true, context) } } // Authorization errors export class AuthorizationError extends ApiError { constructor(message: string = "Access denied", context?: Record) { super(message, 403, true, context) } } // Not found errors export class NotFoundError extends ApiError { constructor(message: string = "Resource not found", context?: Record) { super(message, 404, true, context) } } // Rate limit errors export class RateLimitError extends ApiError { constructor(message: string = "Rate limit exceeded", context?: Record) { super(message, 429, true, context) } } // External service errors export class ExternalServiceError extends ApiError { constructor(service: string, message?: string, context?: Record) { super(message || `External service error: ${service}`, 502, true, { service, ...context }) } } // Database errors export class DatabaseError extends ApiError { constructor(message: string, context?: Record) { super(message, 500, false, context) } } // Configuration errors export class ConfigurationError extends ApiError { constructor(message: string, context?: Record) { super(message, 500, false, context) } } // Business logic errors export class BusinessLogicError extends ApiError { constructor(message: string, context?: Record) { super(message, 400, true, context) } } // Error response interface interface ErrorResponse { error: { message: string code: string statusCode: number requestId?: string timestamp: string context?: Record } } // Global error handler class export class GlobalErrorHandler { /** * Main error handling middleware * Should be registered as the last middleware in the Express app */ static handle(error: Error, req: Request, res: Response, next: NextFunction): void { // If response was already sent, delegate to default Express error handler if (res.headersSent) { return next(error) } // Create error context for logging const errorContext: LogContext = { requestId: req.requestId, traceId: req.traceId, userId: (req as any).userId, endpoint: req.path, operation: "error_handling", } // Handle different types of errors // ========================================== // ERROR TYPE CLASSIFICATION & SENTRY BEHAVIOR: // ========================================== // // 1. ApiError (Custom Error Classes): // - ValidationError, AuthenticationError, DatabaseError, etc. // - Sentry: ✅ 5xx errors sent to Sentry, ❌ 4xx errors NOT sent // - Production Logs: ERROR level with statusCode, duration, error details // - Examples: DatabaseError(500) → Sentry, ValidationError(400) → No Sentry // // 2. Operational Errors (Expected Errors): // - User input errors, business logic errors, expected failures // - Sentry: ❌ NEVER sent to Sentry (expected behavior) // - Production Logs: WARN level with basic error info // - Examples: Invalid JSON, missing required fields, business rule violations // // 3. Programming Errors (Unexpected Errors): // - Unhandled exceptions, bugs, infrastructure failures // - Sentry: ✅ ALWAYS sent to Sentry (critical issues) // - Production Logs: ERROR level with full stack trace and error details // - Examples: Null pointer exceptions, unhandled promise rejections, system crashes // // PRODUCTION LOG SUMMARY: // - ApiError 5xx: ERROR + Sentry (Critical infrastructure issues) // - ApiError 4xx: ERROR + No Sentry (Expected client errors) // - Operational: WARN + No Sentry (Expected business logic issues) // - Programming: ERROR + Sentry (Unexpected system failures) if (error instanceof ApiError) { // Custom error classes (ValidationError, DatabaseError, etc.) // Sentry: 5xx errors YES, 4xx errors NO // Production: ERROR level with statusCode and error details GlobalErrorHandler.handleApiError(error, req, res, errorContext) } else if (GlobalErrorHandler.isOperationalError(error)) { // Expected errors (user input, business logic) // Sentry: NO (expected behavior) // Production: WARN level with basic error info GlobalErrorHandler.handleOperationalError(error, req, res, errorContext) } else { // Unexpected errors (bugs, infrastructure failures) // Sentry: YES (critical issues) // Production: ERROR level with full stack trace GlobalErrorHandler.handleProgrammingError(error, req, res, errorContext) } } /** * Handle custom API errors (ValidationError, DatabaseError, etc.) * * SENTRY BEHAVIOR: * - ✅ 5xx errors (500, 502, 503) → Sent to Sentry (critical infrastructure) * - ❌ 4xx errors (400, 401, 403, 404, 429) → NOT sent to Sentry (expected client errors) * * PRODUCTION LOGS: * - Level: ERROR * - Content: timestamp, level, message, requestId, endpoint, userId, statusCode, duration, error details * - Example: {"timestamp":"2024-01-01T10:00:00.000Z","level":"ERROR","message":"Database connection failed","requestId":"req-123","endpoint":"/cfapi/optimize","userId":"user-456","statusCode":500,"duration":2500,"error":{"name":"DatabaseError","message":"Connection timeout","stack":"..."}} */ private static handleApiError( error: ApiError, req: Request, res: Response, context: LogContext, ): void { const errorResponse = GlobalErrorHandler.formatErrorResponse(error, req) // Use errorWithSentry for critical errors, error for operational errors if (error.statusCode >= 500 || !error.isOperational) { logger.errorWithSentry( `API Error: ${error.message}`, context, { errorType: error.constructor.name, statusCode: error.statusCode, isOperational: error.isOperational, context: error.context, }, error, ) } else { logger.error( `API Error: ${error.message}`, context, { errorType: error.constructor.name, statusCode: error.statusCode, isOperational: error.isOperational, context: error.context, }, error, ) } // Send response res.status(error.statusCode).json(errorResponse) } /** * Handle operational errors (expected errors like user input validation) * * SENTRY BEHAVIOR: * - ❌ NEVER sent to Sentry (these are expected user/business logic errors) * * PRODUCTION LOGS: * - Level: WARN * - Content: timestamp, level, message, requestId, endpoint, userId, errorType, statusCode * - Example: {"timestamp":"2024-01-01T10:00:00.000Z","level":"WARN","message":"Operational Error: Invalid email format","requestId":"req-123","endpoint":"/cfapi/create-user","userId":"user-456","errorType":"ValidationError","statusCode":400} * * EXAMPLES: * - Invalid JSON in request body * - Missing required fields * - Business rule violations * - User permission issues */ private static handleOperationalError( error: Error, req: Request, res: Response, context: LogContext, ): void { const apiError = new ApiError(error.message, 400, true) const errorResponse = GlobalErrorHandler.formatErrorResponse(apiError, req) // Log as warning for operational errors (no Sentry) logger.warn( `Operational Error: ${error.message}`, context, { errorType: error.constructor.name, statusCode: 400, }, error, ) res.status(400).json(errorResponse) } /** * Handle programming errors (unexpected errors like bugs and infrastructure failures) * * SENTRY BEHAVIOR: * - ✅ ALWAYS sent to Sentry (these are critical system issues that need immediate attention) * * PRODUCTION LOGS: * - Level: ERROR * - Content: timestamp, level, message, requestId, endpoint, userId, errorType, statusCode, isOperational, stack * - Example: {"timestamp":"2024-01-01T10:00:00.000Z","level":"ERROR","message":"Programming Error: Cannot read property 'id' of undefined","requestId":"req-123","endpoint":"/cfapi/optimize","userId":"user-456","errorType":"TypeError","statusCode":500,"isOperational":false,"stack":"TypeError: Cannot read property 'id' of undefined\n at UserService.getUser (/app/services/user.js:45:12)\n at ..."} * * EXAMPLES: * - Null pointer exceptions * - Unhandled promise rejections * - System crashes * - Infrastructure failures * - Memory leaks * - Uncaught exceptions */ private static handleProgrammingError( error: Error, req: Request, res: Response, context: LogContext, ): void { const apiError = new ApiError( process.env.NODE_ENV === "production" ? "Internal server error" : error.message, 500, false, ) const errorResponse = GlobalErrorHandler.formatErrorResponse(apiError, req) // Log as error for programming errors (always send to Sentry) logger.errorWithSentry( `Programming Error: ${error.message}`, context, { errorType: error.constructor.name, statusCode: 500, isOperational: false, stack: error.stack, }, error, ) res.status(500).json(errorResponse) } /** * Format error response for API consumers */ private static formatErrorResponse(error: ApiError, req: Request): ErrorResponse { return { error: { message: error.message, code: error.constructor.name, statusCode: error.statusCode, requestId: req.requestId, timestamp: new Date().toISOString(), ...(error.context && { context: error.context }), }, } } /** * Determine if an error is operational (expected) or programming (unexpected) */ private static isOperationalError(error: Error): boolean { if (error instanceof ApiError) { return error.isOperational } // Common operational errors const operationalErrors = [ "ValidationError", "CastError", "JsonWebTokenError", "TokenExpiredError", "MulterError", ] return operationalErrors.includes(error.name) } /** * Handle unhandled promise rejections * * SENTRY BEHAVIOR: * - ✅ ALWAYS sent to Sentry for monitoring * * SYSTEM BEHAVIOR: * - Does NOT exit the process * - Unhandled rejections don't corrupt process state * - Service continues running after logging */ static async handleUnhandledRejection(reason: any, promise: Promise): Promise { const context: LogContext = { operation: "unhandled_rejection", } const error = reason instanceof Error ? reason : new Error(String(reason)) logger.errorWithSentry( "Unhandled Promise Rejection", context, { reason: reason?.toString(), promise: promise.toString(), isOperational: false, }, error, ) // Do NOT exit - unhandled rejections don't corrupt process state } /** * Handle uncaught exceptions * * SENTRY BEHAVIOR: * - ✅ ALWAYS sent to Sentry for monitoring * * SYSTEM BEHAVIOR: * - Does NOT exit the process * - Modern Node.js error handling makes process exit less necessary * - Service continues running after logging */ static async handleUncaughtException(error: Error): Promise { const context: LogContext = { operation: "uncaught_exception", } logger.errorWithSentry( "Uncaught Exception", context, { stack: error.stack, isOperational: false, }, error, ) // Do NOT exit - keep service running } } // Convenience function for creating API errors export function createApiError( message: string, statusCode: number = 500, context?: Record, ): ApiError { return new ApiError(message, statusCode, true, context) }