2025-12-15 16:02:20 +00:00
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 < string , any >
constructor (
message : string ,
statusCode : number = 500 ,
isOperational : boolean = true ,
context? : Record < string , any > ,
) {
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 < string , any > ) {
super ( message , 400 , true , context )
}
}
// Authentication errors
export class AuthenticationError extends ApiError {
constructor ( message : string = "Authentication failed" , context? : Record < string , any > ) {
super ( message , 401 , true , context )
}
}
// Authorization errors
export class AuthorizationError extends ApiError {
constructor ( message : string = "Access denied" , context? : Record < string , any > ) {
super ( message , 403 , true , context )
}
}
// Not found errors
export class NotFoundError extends ApiError {
constructor ( message : string = "Resource not found" , context? : Record < string , any > ) {
super ( message , 404 , true , context )
}
}
// Rate limit errors
export class RateLimitError extends ApiError {
constructor ( message : string = "Rate limit exceeded" , context? : Record < string , any > ) {
super ( message , 429 , true , context )
}
}
// External service errors
export class ExternalServiceError extends ApiError {
constructor ( service : string , message? : string , context? : Record < string , any > ) {
super ( message || ` External service error: ${ service } ` , 502 , true , { service , . . . context } )
}
}
// Database errors
export class DatabaseError extends ApiError {
constructor ( message : string , context? : Record < string , any > ) {
super ( message , 500 , false , context )
}
}
// Configuration errors
export class ConfigurationError extends ApiError {
constructor ( message : string , context? : Record < string , any > ) {
super ( message , 500 , false , context )
}
}
// Business logic errors
export class BusinessLogicError extends ApiError {
constructor ( message : string , context? : Record < string , any > ) {
super ( message , 400 , true , context )
}
}
// Error response interface
interface ErrorResponse {
error : {
message : string
code : string
statusCode : number
requestId? : string
timestamp : string
context? : Record < string , any >
}
}
// 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 :
* - ✅ 5 xx errors ( 500 , 502 , 503 ) → Sent to Sentry ( critical infrastructure )
* - ❌ 4 xx 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 )
}
/ * *
2026-01-27 12:05:48 +00:00
* Handle unhandled promise rejections
2025-12-15 16:02:20 +00:00
*
* SENTRY BEHAVIOR :
2026-01-27 12:05:48 +00:00
* - ✅ ALWAYS sent to Sentry for monitoring
2025-12-15 16:02:20 +00:00
*
* SYSTEM BEHAVIOR :
2026-01-27 12:05:48 +00:00
* - Does NOT exit the process
* - Unhandled rejections don ' t corrupt process state
* - Service continues running after logging
2025-12-15 16:02:20 +00:00
* /
static async handleUnhandledRejection ( reason : any , promise : Promise < any > ) : Promise < void > {
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 ,
)
2026-01-27 12:05:48 +00:00
// Do NOT exit - unhandled rejections don't corrupt process state
2025-12-15 16:02:20 +00:00
}
/ * *
2026-01-27 12:05:48 +00:00
* Handle uncaught exceptions
2025-12-15 16:02:20 +00:00
*
* SENTRY BEHAVIOR :
2026-01-27 12:05:48 +00:00
* - ✅ ALWAYS sent to Sentry for monitoring
2025-12-15 16:02:20 +00:00
*
* SYSTEM BEHAVIOR :
2026-01-27 12:05:48 +00:00
* - Does NOT exit the process
* - Modern Node . js error handling makes process exit less necessary
* - Service continues running after logging
2025-12-15 16:02:20 +00:00
* /
static async handleUncaughtException ( error : Error ) : Promise < void > {
const context : LogContext = {
operation : "uncaught_exception" ,
}
logger . errorWithSentry (
"Uncaught Exception" ,
context ,
{
stack : error.stack ,
isOperational : false ,
} ,
error ,
)
2026-01-27 12:05:48 +00:00
// Do NOT exit - keep service running
2025-12-15 16:02:20 +00:00
}
}
// Convenience function for creating API errors
export function createApiError (
message : string ,
statusCode : number = 500 ,
context? : Record < string , any > ,
) : ApiError {
return new ApiError ( message , statusCode , true , context )
}