/** * Error Handling Utilities * Provides consistent error handling, logging, and user-friendly messaging across the application */ import { ApiError } from '@/lib/http-client'; // Error severity levels export enum ErrorSeverity { LOW = 'low', // User can retry, minor inconvenience MEDIUM = 'medium', // User experience impacted but recoverable HIGH = 'high', // Critical functionality broken CRITICAL = 'critical' // Application unusable } // Error categories for better handling export enum ErrorCategory { NETWORK = 'network', AUTHENTICATION = 'authentication', AUTHORIZATION = 'authorization', VALIDATION = 'validation', SERVER = 'server', CLIENT = 'client', TIMEOUT = 'timeout', RATE_LIMIT = 'rate_limit', UNKNOWN = 'unknown', } // Enhanced error interface export interface AppError { id: string; message: string; userMessage: string; technicalMessage?: string; category: ErrorCategory; severity: ErrorSeverity; statusCode?: number; timestamp: Date; context?: Record; retryable: boolean; originalError?: Error; } // Error handler configuration interface ErrorHandlerConfig { enableLogging: boolean; enableSentry?: boolean; enableAnalytics?: boolean; userMessageOverrides?: Record; } /** * Error Handler Class */ export class ErrorHandler { private config: Required> & Pick; constructor(config: ErrorHandlerConfig = {}) { this.config = { enableLogging: import.meta.env.DEV, enableSentry: false, enableAnalytics: false, userMessageOverrides: {}, ...config, }; } /** * Handle any error and convert to AppError */ handle(error: unknown, context?: Record): AppError { const appError = this.normalizeError(error, context); // Log error if (this.config.enableLogging) { this.logError(appError); } // Send to error tracking services if (this.config.enableSentry) { this.sendToSentry(appError); } if (this.config.enableAnalytics) { this.sendToAnalytics(appError); } return appError; } /** * Convert various error types to AppError */ private normalizeError(error: unknown, context?: Record): AppError { const timestamp = new Date(); const id = this.generateErrorId(); // Handle ApiError if (error instanceof ApiError) { return { id, message: error.message, userMessage: this.getUserMessage(error), technicalMessage: import.meta.env.DEV ? error.data?.error : undefined, category: this.categorizeApiError(error), severity: this.getSeverity(error), statusCode: error.status, timestamp, context, retryable: error.retryable, originalError: error, }; } // Handle ZodError if (error && typeof error === 'object' && 'name' in error && error.name === 'ZodError') { const zodError = error as { message: string; issues?: unknown[] }; return { id, message: 'Validation failed', userMessage: 'Please check your input and try again.', technicalMessage: import.meta.env.DEV ? zodError.message : undefined, category: ErrorCategory.VALIDATION, severity: ErrorSeverity.MEDIUM, timestamp, context, retryable: false, originalError: new Error(zodError.message), }; } // Handle standard Error if (error instanceof Error) { return { id, message: error.message, userMessage: this.getGenericUserMessage(error), technicalMessage: import.meta.env.DEV ? error.stack : undefined, category: this.categorizeGenericError(error), severity: ErrorSeverity.MEDIUM, timestamp, context, retryable: this.isRetryableError(error), originalError: error, }; } // Handle string error if (typeof error === 'string') { return { id, message: error, userMessage: error, category: ErrorCategory.UNKNOWN, severity: ErrorSeverity.MEDIUM, timestamp, context, retryable: false, }; } // Handle unknown error return { id, message: 'An unknown error occurred', userMessage: 'Something went wrong. Please try again.', category: ErrorCategory.UNKNOWN, severity: ErrorSeverity.MEDIUM, timestamp, context, retryable: true, }; } /** * Categorize API errors */ private categorizeApiError(error: ApiError): ErrorCategory { switch (error.status) { case 0: return ErrorCategory.NETWORK; case 401: return ErrorCategory.AUTHENTICATION; case 403: return ErrorCategory.AUTHORIZATION; case 408: case 504: return ErrorCategory.TIMEOUT; case 429: return ErrorCategory.RATE_LIMIT; case 422: return ErrorCategory.VALIDATION; case 400: case 404: return ErrorCategory.CLIENT; default: return error.status >= 500 ? ErrorCategory.SERVER : ErrorCategory.CLIENT; } } /** * Categorize generic errors */ private categorizeGenericError(error: Error): ErrorCategory { const message = error.message.toLowerCase(); if (message.includes('network') || message.includes('fetch')) { return ErrorCategory.NETWORK; } if (message.includes('timeout')) { return ErrorCategory.TIMEOUT; } if (message.includes('validation') || message.includes('invalid')) { return ErrorCategory.VALIDATION; } return ErrorCategory.UNKNOWN; } /** * Get severity level for API errors */ private getSeverity(error: ApiError): ErrorSeverity { if (error.status >= 500) return ErrorSeverity.HIGH; if (error.status === 429) return ErrorSeverity.MEDIUM; if (error.status === 401 || error.status === 403) return ErrorSeverity.HIGH; if (error.status === 0) return ErrorSeverity.CRITICAL; // Network completely down return ErrorSeverity.LOW; } /** * Check if generic error is retryable */ private isRetryableError(error: Error): boolean { const message = error.message.toLowerCase(); return message.includes('network') || message.includes('timeout') || message.includes('temporary'); } /** * Get user-friendly message for API errors */ private getUserMessage(error: ApiError): string { // Check for custom overrides if (this.config.userMessageOverrides[error.status]) { return this.config.userMessageOverrides[error.status]; } // Use the ApiError's user-friendly message if available if (error.message && !error.message.includes('HTTP')) { return error.message; } // Default messages based on status switch (error.status) { case 0: return 'Unable to connect. Please check your internet connection.'; case 401: return 'Please log in to continue.'; case 403: return 'You don\'t have permission to perform this action.'; case 404: return 'The requested item was not found.'; case 408: case 504: return 'The request timed out. Please try again.'; case 429: return 'Too many requests. Please wait a moment before trying again.'; case 422: return 'Please check your input and try again.'; case 500: return 'A server error occurred. Please try again later.'; case 502: case 503: return 'Service temporarily unavailable. Please try again later.'; default: return 'Something went wrong. Please try again.'; } } /** * Get user-friendly message for generic errors */ private getGenericUserMessage(error: Error): string { const message = error.message.toLowerCase(); if (message.includes('network') || message.includes('fetch')) { return 'Network connection failed. Please check your internet connection.'; } if (message.includes('timeout')) { return 'The request timed out. Please try again.'; } if (message.includes('validation')) { return 'Please check your input and try again.'; } return 'An unexpected error occurred. Please try again.'; } /** * Log error with appropriate level */ private logError(error: AppError): void { const logData = { id: error.id, message: error.message, category: error.category, severity: error.severity, statusCode: error.statusCode, retryable: error.retryable, context: error.context, timestamp: error.timestamp.toISOString(), }; switch (error.severity) { case ErrorSeverity.CRITICAL: console.error('🚨 CRITICAL ERROR:', logData); break; case ErrorSeverity.HIGH: console.error('❌ HIGH PRIORITY ERROR:', logData); break; case ErrorSeverity.MEDIUM: console.warn('âš ī¸ MEDIUM PRIORITY ERROR:', logData); break; default: console.info('â„šī¸ LOW PRIORITY ERROR:', logData); } // Log technical details in development if (import.meta.env.DEV && error.technicalMessage) { console.debug('Technical details:', error.technicalMessage); } } /** * Send error to Sentry (placeholder) */ private sendToSentry(error: AppError): void { // Placeholder for Sentry integration if (typeof window !== 'undefined' && (window as any).Sentry) { (window as any).Sentry.captureException(error.originalError || new Error(error.message), { tags: { category: error.category, severity: error.severity, }, extra: { errorId: error.id, context: error.context, }, }); } } /** * Send error to analytics (placeholder) */ private sendToAnalytics(error: AppError): void { // Placeholder for analytics integration if (typeof window !== 'undefined' && (window as any).gtag) { (window as any).gtag('event', 'exception', { description: error.message, fatal: error.severity === ErrorSeverity.CRITICAL, }); } } /** * Generate unique error ID */ private generateErrorId(): string { return `error_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } } // Global error handler instance export const errorHandler = new ErrorHandler({ enableLogging: import.meta.env.DEV, }); /** * Hook for handling errors in React components */ export function useErrorHandler() { return { handleError: (error: unknown, context?: Record) => errorHandler.handle(error, context), }; } /** * Utility function for consistent error handling in async operations */ export async function withErrorHandling( operation: () => Promise, context?: Record ): Promise { try { return await operation(); } catch (error) { const appError = errorHandler.handle(error, context); throw appError; } } /** * Error boundary helper for consistent error reporting */ export function reportError(error: unknown, context?: Record): AppError { return errorHandler.handle(error, context); }