const logger = require('../utils/logger'); /** * Error types and their corresponding HTTP status codes */ const ErrorTypes = { VALIDATION_ERROR: 'ValidationError', NOT_FOUND_ERROR: 'NotFoundError', CONFLICT_ERROR: 'ConflictError', UNAUTHORIZED_ERROR: 'UnauthorizedError', FORBIDDEN_ERROR: 'ForbiddenError', RATE_LIMIT_ERROR: 'RateLimitError', FILE_UPLOAD_ERROR: 'FileUploadError', DATABASE_ERROR: 'DatabaseError', EXTERNAL_SERVICE_ERROR: 'ExternalServiceError', BUSINESS_LOGIC_ERROR: 'BusinessLogicError' }; /** * Custom error class for application-specific errors */ class AppError extends Error { constructor(message, type = 'ApplicationError', statusCode = 500, isOperational = true, details = null) { super(message); this.name = this.constructor.name; this.type = type; this.statusCode = statusCode; this.isOperational = isOperational; this.details = details; this.timestamp = new Date().toISOString(); Error.captureStackTrace(this, this.constructor); } } /** * Create specific error types */ class ValidationError extends AppError { constructor(message, details = null) { super(message, ErrorTypes.VALIDATION_ERROR, 400, true, details); } } class NotFoundError extends AppError { constructor(message, details = null) { super(message, ErrorTypes.NOT_FOUND_ERROR, 404, true, details); } } class ConflictError extends AppError { constructor(message, details = null) { super(message, ErrorTypes.CONFLICT_ERROR, 409, true, details); } } class DatabaseError extends AppError { constructor(message, details = null) { super(message, ErrorTypes.DATABASE_ERROR, 500, true, details); } } class BusinessLogicError extends AppError { constructor(message, details = null) { super(message, ErrorTypes.BUSINESS_LOGIC_ERROR, 422, true, details); } } /** * Error response formatter */ const formatErrorResponse = (error, req) => { const isDevelopment = process.env.NODE_ENV !== 'production'; const baseResponse = { success: false, error: error.type || 'InternalServerError', message: error.message || 'An unexpected error occurred', timestamp: error.timestamp || new Date().toISOString(), path: req.originalUrl, method: req.method }; // Add details for operational errors if (error.isOperational && error.details) { baseResponse.details = error.details; } // Add stack trace in development if (isDevelopment && error.stack) { baseResponse.stack = error.stack; } // Add request ID if available if (req.requestId) { baseResponse.requestId = req.requestId; } return baseResponse; }; /** * Determine if error should be retried */ const isRetryableError = (error) => { const retryableTypes = [ ErrorTypes.DATABASE_ERROR, ErrorTypes.EXTERNAL_SERVICE_ERROR ]; const retryableCodes = ['ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND']; return retryableTypes.includes(error.type) || retryableCodes.includes(error.code) || (error.statusCode >= 500 && error.statusCode < 600); }; /** * Get user-friendly error message */ const getUserFriendlyMessage = (error) => { const friendlyMessages = { [ErrorTypes.VALIDATION_ERROR]: 'The provided data is invalid. Please check your input and try again.', [ErrorTypes.NOT_FOUND_ERROR]: 'The requested resource could not be found.', [ErrorTypes.CONFLICT_ERROR]: 'This operation conflicts with existing data. Please check for duplicates.', [ErrorTypes.UNAUTHORIZED_ERROR]: 'Authentication is required to access this resource.', [ErrorTypes.FORBIDDEN_ERROR]: 'You do not have permission to perform this action.', [ErrorTypes.RATE_LIMIT_ERROR]: 'Too many requests. Please wait a moment before trying again.', [ErrorTypes.FILE_UPLOAD_ERROR]: 'There was a problem with the uploaded file. Please check the file format and size.', [ErrorTypes.DATABASE_ERROR]: 'A database error occurred. Please try again later.', [ErrorTypes.EXTERNAL_SERVICE_ERROR]: 'An external service is temporarily unavailable. Please try again later.', [ErrorTypes.BUSINESS_LOGIC_ERROR]: 'This operation cannot be completed due to business rules.' }; return friendlyMessages[error.type] || error.message || 'An unexpected error occurred. Please try again later.'; }; /** * Main error handling middleware */ const errorHandler = (error, req, res, next) => { // Log the error with context const errorContext = { url: req.originalUrl, method: req.method, ip: req.ip, userAgent: req.get('User-Agent'), body: req.method !== 'GET' ? req.body : undefined, params: req.params, query: req.query, requestId: req.requestId }; logger.logError(error, errorContext); // Handle specific error types let appError = error; // Convert known errors to AppError instances if (error.name === 'ValidationError') { appError = new ValidationError(error.message, error.details); } else if (error.name === 'CastError') { appError = new ValidationError('Invalid data format provided'); } else if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') { appError = new ConflictError('A record with this information already exists'); } else if (error.code === 'SQLITE_CONSTRAINT') { appError = new ValidationError('Data constraint violation'); } else if (error.code === 'ENOENT') { appError = new NotFoundError('Requested file or resource not found'); } else if (error.code === 'LIMIT_FILE_SIZE') { appError = new ValidationError('File size exceeds the maximum allowed limit'); } else if (error.code === 'LIMIT_UNEXPECTED_FILE') { appError = new ValidationError('Unexpected file upload'); } else if (!error.isOperational) { // Convert unknown errors to generic AppError appError = new AppError( process.env.NODE_ENV === 'production' ? 'An unexpected error occurred' : error.message, 'InternalServerError', 500, false ); } // Format response const errorResponse = formatErrorResponse(appError, req); // Override message with user-friendly version for production if (process.env.NODE_ENV === 'production') { errorResponse.message = getUserFriendlyMessage(appError); } // Add retry information for retryable errors if (isRetryableError(appError)) { errorResponse.retryable = true; errorResponse.retryAfter = 5; // seconds } // Send error response res.status(appError.statusCode || 500).json(errorResponse); }; /** * 404 handler for unmatched routes */ const notFoundHandler = (req, res, next) => { const error = new NotFoundError(`Route ${req.method} ${req.originalUrl} not found`); next(error); }; /** * Async error wrapper to catch async errors in route handlers */ const asyncHandler = (fn) => { return (req, res, next) => { Promise.resolve(fn(req, res, next)).catch(next); }; }; /** * Request timeout handler */ const timeoutHandler = (timeout = 30000) => { return (req, res, next) => { req.setTimeout(timeout, () => { const error = new AppError('Request timeout', 'TimeoutError', 408); next(error); }); next(); }; }; module.exports = { ErrorTypes, AppError, ValidationError, NotFoundError, ConflictError, DatabaseError, BusinessLogicError, errorHandler, notFoundHandler, asyncHandler, timeoutHandler, isRetryableError, getUserFriendlyMessage };