/** * SERVICE: Error Handler * * PURPOSE: Centralized error handling and user messaging * DEPENDENCIES: UI state store, constants * * FEATURES: * - Convert errors to user-friendly messages * - Log errors with context * - Handle different error types (network, auth, validation) * - Retry logic for transient failures * - Error recovery suggestions */ import type { ApiError } from '../utils/types'; import { ERROR_MESSAGES } from '../utils/constants'; // ============================================================================ // ERROR TYPES // ============================================================================ export enum ErrorType { NETWORK = 'network', AUTH = 'auth', VALIDATION = 'validation', NOTFOUND = 'not_found', PERMISSION = 'permission', SERVER = 'server', UNKNOWN = 'unknown', } export interface ErrorContext { type: ErrorType; message: string; originalError?: Error; statusCode?: number; userMessage: string; retryable: boolean; recoverySteps?: string[]; } // ============================================================================ // ERROR CLASSIFICATION // ============================================================================ /** * FUNCTION: classifyError * * Determine error type and user message * * PARAMS: * - error: unknown - The error to classify * * RETURNS: ErrorContext with type, message, and user guidance */ export function classifyError(error: unknown): ErrorContext { // Handle network errors if (error instanceof TypeError && error.message.includes('fetch')) { return { type: ErrorType.NETWORK, message: 'Network request failed', originalError: error, userMessage: ERROR_MESSAGES.NETWORK_ERROR, retryable: true, recoverySteps: [ 'Check your internet connection', 'Try again in a few moments', 'If problem persists, refresh the page', ], }; } // Handle timeout errors if (error instanceof Error && error.message.includes('timeout')) { return { type: ErrorType.NETWORK, message: 'Request timeout', originalError: error, userMessage: 'Request took too long. Please try again.', retryable: true, recoverySteps: ['Check internet speed', 'Try again', 'Refresh page if needed'], }; } // Handle auth errors (401) if (error instanceof Error && error.message.includes('401')) { return { type: ErrorType.AUTH, message: 'Invalid or expired token', originalError: error, userMessage: ERROR_MESSAGES.AUTH_FAILED, retryable: false, recoverySteps: ['Please sign in again'], }; } // Handle permission errors (403) if (error instanceof Error && error.message.includes('403')) { return { type: ErrorType.PERMISSION, message: 'Access denied', originalError: error, userMessage: ERROR_MESSAGES.PERMISSION_DENIED, retryable: false, recoverySteps: [ 'Check that you have permission to access this resource', 'Contact administrator if needed', ], }; } // Handle not found errors (404) if (error instanceof Error && error.message.includes('404')) { return { type: ErrorType.NOTFOUND, message: 'Resource not found', originalError: error, userMessage: 'The requested resource was not found.', retryable: false, recoverySteps: ['Go back and try again', 'Refresh the page'], }; } // Handle server errors (5xx) if (error instanceof Error && error.message.includes('5')) { return { type: ErrorType.SERVER, message: 'Server error', originalError: error, userMessage: ERROR_MESSAGES.SERVER_ERROR, retryable: true, recoverySteps: [ 'The server is having issues', 'Please try again in a few moments', ], }; } // Handle validation errors if (error instanceof Error && error.message.includes('validation')) { return { type: ErrorType.VALIDATION, message: 'Validation failed', originalError: error, userMessage: 'Please check your input and try again.', retryable: true, recoverySteps: ['Review the form', 'Correct any errors', 'Submit again'], }; } // Default unknown error return { type: ErrorType.UNKNOWN, message: error instanceof Error ? error.message : String(error), originalError: error instanceof Error ? error : new Error(String(error)), userMessage: ERROR_MESSAGES.UNKNOWN_ERROR, retryable: true, recoverySteps: ['Try again', 'Refresh the page if needed'], }; } // ============================================================================ // LOGGING // ============================================================================ /** * FUNCTION: logError * * Log error with context and timestamp * Helps with debugging and error tracking * * PARAMS: * - context: ErrorContext - Error details * - additionalContext?: string - Extra info */ export function logError(context: ErrorContext, additionalContext?: string): void { const timestamp = new Date().toISOString(); const prefix = `[ERROR ${timestamp}]`; console.error(`${prefix} [${context.type.toUpperCase()}]`, { message: context.message, userMessage: context.userMessage, statusCode: context.statusCode, retryable: context.retryable, additional: additionalContext, stack: context.originalError?.stack, }); } /** * FUNCTION: captureErrorMetrics * * Capture error metrics for analytics * Could be sent to error tracking service * * PARAMS: * - context: ErrorContext - Error details */ export function captureErrorMetrics(context: ErrorContext): void { // Could integrate with error tracking service here // (Sentry, LogRocket, etc.) console.debug('[Metrics] Error captured:', { type: context.type, retryable: context.retryable, timestamp: new Date().toISOString(), }); } // ============================================================================ // ERROR RECOVERY // ============================================================================ /** * FUNCTION: shouldRetry * * Determine if error should be retried * Checks error type and attempt count * * PARAMS: * - error: ErrorContext - Classified error * - attempts: number - Number of retry attempts made * * RETURNS: boolean - true if should retry */ export function shouldRetry(error: ErrorContext, attempts: number): boolean { // Don't retry too many times if (attempts >= 3) return false; // Only retry retryable errors return error.retryable; } /** * FUNCTION: getRetryDelay * * Calculate exponential backoff delay * Reduces hammering server on repeated failures * * PARAMS: * - attempt: number - Which attempt number (1, 2, 3...) * * RETURNS: number - Milliseconds to wait before retry */ export function getRetryDelay(attempt: number): number { // Exponential backoff: 100ms, 200ms, 400ms, 800ms... const baseDelay = 100; const delay = baseDelay * Math.pow(2, attempt - 1); // Add random jitter (±20%) to prevent thundering herd const jitter = delay * 0.2 * (Math.random() - 0.5); return Math.max(100, Math.min(delay + jitter, 5000)); } /** * FUNCTION: retryOperation * * Retry operation with exponential backoff * * PARAMS: * - operation: () => Promise - Function to retry * - maxAttempts: number - Max number of attempts (default: 3) * * RETURNS: Promise - Result of successful operation * THROWS: Error if all attempts fail */ export async function retryOperation( operation: () => Promise, maxAttempts: number = 3 ): Promise { let lastError: ErrorContext | null = null; for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { return await operation(); } catch (error) { lastError = classifyError(error); if (!shouldRetry(lastError, attempt)) { logError(lastError, `Attempt ${attempt} failed, not retrying`); throw error; } const delay = getRetryDelay(attempt); console.log(`[Retry] Attempt ${attempt} failed, waiting ${delay}ms before retry`); await new Promise((resolve) => setTimeout(resolve, delay)); } } // All attempts failed if (lastError) { logError(lastError, `All ${maxAttempts} attempts failed`); } throw new Error(`Operation failed after ${maxAttempts} attempts`); } // ============================================================================ // USER MESSAGES // ============================================================================ /** * FUNCTION: getUserMessage * * Get user-friendly error message * Safe to display in UI * * PARAMS: * - error: unknown - The error * * RETURNS: string - User-friendly message */ export function getUserMessage(error: unknown): string { const context = classifyError(error); return context.userMessage; } /** * FUNCTION: getRecoverySteps * * Get suggested recovery steps * Help user recover from error * * PARAMS: * - error: unknown - The error * * RETURNS: string[] - List of suggested steps */ export function getRecoverySteps(error: unknown): string[] { const context = classifyError(error); return context.recoverySteps || []; }