/** * Standardized Error Handler * * Provides consistent error handling patterns across the TimeSafari codebase * to improve debugging, user experience, and maintainability. * * @author Matthew Raymer * @since 2025-08-25 */ import { AxiosError } from "axios"; import { logger } from "./logger"; /** * Standard error context for consistent logging */ export interface ErrorContext { component: string; operation: string; timestamp: string; [key: string]: unknown; } /** * Enhanced error information for better debugging */ export interface EnhancedErrorInfo { errorType: "AxiosError" | "NetworkError" | "ValidationError" | "UnknownError"; status?: number; statusText?: string; errorData?: unknown; errorMessage: string; errorStack?: string; requestContext?: { url?: string; method?: string; headers?: Record; }; } /** * Standardized error handler for API operations * * @param error - The error that occurred * @param context - Context information about the operation * @param operation - Description of the operation being performed * @returns Enhanced error information for consistent handling */ export function handleApiError( error: unknown, context: ErrorContext, operation: string, ): EnhancedErrorInfo { const baseContext = { ...context, operation, timestamp: new Date().toISOString(), }; if (error instanceof AxiosError) { const axiosError = error as AxiosError; const status = axiosError.response?.status; const statusText = axiosError.response?.statusText; const errorData = axiosError.response?.data; const enhancedError: EnhancedErrorInfo = { errorType: "AxiosError", status, statusText, errorData, errorMessage: axiosError.message, errorStack: axiosError.stack, requestContext: { url: axiosError.config?.url, method: axiosError.config?.method, headers: axiosError.config?.headers, }, }; // Log with consistent format logger.error( `[${context.component}] ❌ ${operation} failed (AxiosError):`, { ...baseContext, ...enhancedError, }, ); return enhancedError; } if (error instanceof Error) { const enhancedError: EnhancedErrorInfo = { errorType: "UnknownError", errorMessage: error.message, errorStack: error.stack, }; logger.error(`[${context.component}] ❌ ${operation} failed (Error):`, { ...baseContext, ...enhancedError, }); return enhancedError; } // Handle unknown error types const enhancedError: EnhancedErrorInfo = { errorType: "UnknownError", errorMessage: String(error), }; logger.error(`[${context.component}] ❌ ${operation} failed (Unknown):`, { ...baseContext, ...enhancedError, }); return enhancedError; } /** * Extract human-readable error message from various error response formats * * @param errorData - Error response data * @returns Human-readable error message */ export function extractErrorMessage(errorData: unknown): string { if (typeof errorData === "string") { return errorData; } if (typeof errorData === "object" && errorData !== null) { const obj = errorData as Record; // Try common error message fields if (obj.message && typeof obj.message === "string") { return obj.message; } if (obj.error && typeof obj.error === "string") { return obj.error; } if (obj.detail && typeof obj.detail === "string") { return obj.detail; } if (obj.reason && typeof obj.reason === "string") { return obj.reason; } // Fallback to stringified object return JSON.stringify(errorData); } return String(errorData); } /** * Create user-friendly error message from enhanced error info * * @param errorInfo - Enhanced error information * @param fallbackMessage - Fallback message if error details are insufficient * @returns User-friendly error message */ export function createUserMessage( errorInfo: EnhancedErrorInfo, fallbackMessage: string, ): string { if (errorInfo.errorType === "AxiosError") { const status = errorInfo.status; const statusText = errorInfo.statusText; const errorMessage = extractErrorMessage(errorInfo.errorData); if (status && statusText) { if (errorMessage && errorMessage !== "{}") { return `${fallbackMessage}: ${status} ${statusText} - ${errorMessage}`; } return `${fallbackMessage}: ${status} ${statusText}`; } } if ( errorInfo.errorMessage && errorInfo.errorMessage !== "Request failed with status code 0" ) { return `${fallbackMessage}: ${errorInfo.errorMessage}`; } return fallbackMessage; } /** * Handle specific HTTP status codes with appropriate user messages * * @param status - HTTP status code * @param errorData - Error response data * @param operation - Description of the operation * @returns User-friendly error message */ export function handleHttpStatus( status: number, errorData: unknown, operation: string, ): string { const errorMessage = extractErrorMessage(errorData); switch (status) { case 400: return errorMessage || `${operation} failed: Bad request`; case 401: return `${operation} failed: Authentication required`; case 403: return `${operation} failed: Access denied`; case 404: return errorMessage || `${operation} failed: Resource not found`; case 409: return errorMessage || `${operation} failed: Conflict with existing data`; case 422: return errorMessage || `${operation} failed: Validation error`; case 429: return `${operation} failed: Too many requests. Please try again later.`; case 500: return `${operation} failed: Server error. Please try again later.`; case 502: case 503: case 504: return `${operation} failed: Service temporarily unavailable. Please try again later.`; default: return errorMessage || `${operation} failed: HTTP ${status}`; } } /** * Check if an error is a network-related error * * @param error - The error to check * @returns True if the error is network-related */ export function isNetworkError(error: unknown): boolean { if (error instanceof AxiosError) { return !error.response && !error.request; } if (error instanceof Error) { const message = error.message.toLowerCase(); return ( message.includes("network") || message.includes("timeout") || message.includes("connection") || message.includes("fetch") ); } return false; } /** * Check if an error is a timeout error * * @param error - The error to check * @returns True if the error is a timeout */ export function isTimeoutError(error: unknown): boolean { if (error instanceof AxiosError) { return ( error.code === "ECONNABORTED" || error.message.toLowerCase().includes("timeout") ); } if (error instanceof Error) { return error.message.toLowerCase().includes("timeout"); } return false; } /** * Create standardized error context for components * * @param component - Component name * @param operation - Operation being performed * @param additionalContext - Additional context information * @returns Standardized error context */ export function createErrorContext( component: string, operation: string, additionalContext: Record = {}, ): ErrorContext { return { component, operation, timestamp: new Date().toISOString(), ...additionalContext, }; }