/** * Enhanced API Error with detailed classification and context */ export type ApiErrorType = | 'NETWORK_ERROR' // Connection failed, timeout, CORS | 'AUTH_ERROR' // 401, 403 - Authentication/Authorization issues | 'VALIDATION_ERROR' // 400 - Bad request, invalid data | 'NOT_FOUND' // 404 - Resource not found | 'SERVER_ERROR' // 500, 502, 503 - Server-side issues | 'RATE_LIMIT_ERROR' // 429 - Too many requests | 'CANCELED_ERROR' // Request was canceled | 'TIMEOUT_ERROR' // Request timeout | 'UNKNOWN_ERROR'; // Everything else export interface ApiErrorContext { endpoint?: string; method?: string; requestBody?: unknown; timestamp: string; statusCode?: number; responseText?: string; retryCount?: number; wasRetry?: boolean; troubleshooting?: string; source?: string; componentStack?: string; isRetryable?: boolean; isConnectivity?: boolean; developerHint?: string; } export class ApiError extends Error { public readonly type: ApiErrorType; public readonly context: ApiErrorContext; public readonly originalError?: Error; constructor( message: string, type: ApiErrorType, context: ApiErrorContext, originalError?: Error ) { super(message); this.name = 'ApiError'; this.type = type; this.context = context; this.originalError = originalError; // Maintains proper stack trace for where our error was thrown if (Error.captureStackTrace) { Error.captureStackTrace(this, ApiError); } } /** * User-friendly message for production environments */ getUserMessage(): string { switch (this.type) { case 'NETWORK_ERROR': return 'Unable to connect to the server. Please check your internet connection.'; case 'AUTH_ERROR': return 'Authentication required. Please log in again.'; case 'VALIDATION_ERROR': return 'The data you provided is invalid. Please check your input.'; case 'NOT_FOUND': return 'The requested resource was not found.'; case 'SERVER_ERROR': return 'Server is experiencing issues. Please try again later.'; case 'RATE_LIMIT_ERROR': return 'Too many requests. Please wait a moment and try again.'; case 'TIMEOUT_ERROR': return 'Request timed out. Please try again.'; case 'CANCELED_ERROR': return 'Request was canceled.'; default: return 'An unexpected error occurred. Please try again.'; } } /** * Developer-friendly message with full context */ getDeveloperMessage(): string { const base = `[${this.type}] ${this.message}`; const ctx = [ this.context.method, this.context.endpoint, this.context.statusCode ? `status:${this.context.statusCode}` : null, this.context.retryCount ? `retry:${this.context.retryCount}` : null, ] .filter(Boolean) .join(' '); return ctx ? `${base} ${ctx}` : base; } /** * Check if this error is retryable */ isRetryable(): boolean { const retryableTypes: ApiErrorType[] = [ 'NETWORK_ERROR', 'SERVER_ERROR', 'RATE_LIMIT_ERROR', 'TIMEOUT_ERROR', ]; return retryableTypes.includes(this.type); } /** * Check if this error indicates connectivity issues */ isConnectivityIssue(): boolean { return this.type === 'NETWORK_ERROR' || this.type === 'TIMEOUT_ERROR'; } /** * Get error severity for logging */ getSeverity(): 'error' | 'warn' | 'info' { switch (this.type) { case 'AUTH_ERROR': case 'VALIDATION_ERROR': case 'NOT_FOUND': return 'warn'; case 'RATE_LIMIT_ERROR': case 'CANCELED_ERROR': return 'info'; default: return 'error'; } } } /** * Type guards for error classification */ export function isApiError(error: unknown): error is ApiError { return error instanceof ApiError; } export function isNetworkError(error: unknown): boolean { return isApiError(error) && error.type === 'NETWORK_ERROR'; } export function isAuthError(error: unknown): boolean { return isApiError(error) && error.type === 'AUTH_ERROR'; } export function isRetryableError(error: unknown): boolean { return isApiError(error) && error.isRetryable(); }