/** * Centralized Error Handler Service * * Provides error classification, user-friendly message translation, * and recovery strategy recommendations. */ import { AppError, ErrorCategory, ErrorCode, ErrorCodes, ErrorSeverity, FieldError, RetryPolicy, DefaultRetryPolicies, } from '@/types/errors'; import { ApiError } from '@/types'; // Generate unique error ID function generateErrorId(): string { return `err_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } // User-friendly messages for error codes const userMessages: Record = { // Network [ErrorCodes.NETWORK_ERROR]: 'Unable to connect. Please check your internet connection.', [ErrorCodes.NETWORK_TIMEOUT]: 'The request timed out. Please try again.', [ErrorCodes.NETWORK_OFFLINE]: 'You appear to be offline. Please check your connection.', // Authentication [ErrorCodes.UNAUTHORIZED]: 'Your session has expired. Please log in again.', [ErrorCodes.TOKEN_EXPIRED]: 'Your session has expired. Please log in again.', [ErrorCodes.INVALID_CREDENTIALS]: 'Invalid email or password.', [ErrorCodes.SESSION_EXPIRED]: 'Your session has expired. Please log in again.', // Permission [ErrorCodes.FORBIDDEN]: 'You don\'t have permission to perform this action.', [ErrorCodes.INSUFFICIENT_PERMISSIONS]: 'You don\'t have sufficient permissions.', // Resource [ErrorCodes.NOT_FOUND]: 'The requested resource was not found.', [ErrorCodes.BENEFICIARY_NOT_FOUND]: 'Beneficiary not found.', [ErrorCodes.DEPLOYMENT_NOT_FOUND]: 'Deployment not found.', [ErrorCodes.USER_NOT_FOUND]: 'User not found.', // Conflict [ErrorCodes.CONFLICT]: 'A conflict occurred. Please refresh and try again.', [ErrorCodes.DUPLICATE_ENTRY]: 'This entry already exists.', [ErrorCodes.VERSION_MISMATCH]: 'Data has changed. Please refresh and try again.', // Validation [ErrorCodes.VALIDATION_ERROR]: 'Please check your input and try again.', [ErrorCodes.INVALID_INPUT]: 'Invalid input. Please check and try again.', [ErrorCodes.MISSING_REQUIRED_FIELD]: 'Please fill in all required fields.', [ErrorCodes.INVALID_EMAIL]: 'Please enter a valid email address.', [ErrorCodes.INVALID_PHONE]: 'Please enter a valid phone number.', [ErrorCodes.INVALID_OTP]: 'Invalid verification code. Please try again.', // Server [ErrorCodes.SERVER_ERROR]: 'Something went wrong on our end. Please try again later.', [ErrorCodes.SERVICE_UNAVAILABLE]: 'Service is temporarily unavailable. Please try again later.', [ErrorCodes.MAINTENANCE]: 'We\'re currently performing maintenance. Please try again soon.', // Rate limiting [ErrorCodes.RATE_LIMITED]: 'Too many requests. Please wait a moment and try again.', [ErrorCodes.TOO_MANY_REQUESTS]: 'Too many requests. Please wait a moment and try again.', // BLE [ErrorCodes.BLE_NOT_AVAILABLE]: 'Bluetooth is not available on this device.', [ErrorCodes.BLE_NOT_ENABLED]: 'Please enable Bluetooth to continue.', [ErrorCodes.BLE_PERMISSION_DENIED]: 'Bluetooth permission is required.', [ErrorCodes.BLE_CONNECTION_FAILED]: 'Failed to connect to device. Please try again.', [ErrorCodes.BLE_DEVICE_NOT_FOUND]: 'Device not found. Make sure it\'s powered on.', [ErrorCodes.BLE_TIMEOUT]: 'Connection timed out. Please try again.', // Sensor [ErrorCodes.SENSOR_SETUP_FAILED]: 'Sensor setup failed. Please try again.', [ErrorCodes.SENSOR_OFFLINE]: 'Sensor is offline. Check the device connection.', [ErrorCodes.SENSOR_NOT_RESPONDING]: 'Sensor is not responding. Please try again.', [ErrorCodes.SENSOR_WIFI_FAILED]: 'Failed to configure WiFi. Please check credentials.', // Subscription [ErrorCodes.SUBSCRIPTION_REQUIRED]: 'A subscription is required to access this feature.', [ErrorCodes.SUBSCRIPTION_EXPIRED]: 'Your subscription has expired.', [ErrorCodes.PAYMENT_FAILED]: 'Payment failed. Please try again or use a different method.', [ErrorCodes.PAYMENT_CANCELED]: 'Payment was canceled.', // General [ErrorCodes.UNKNOWN_ERROR]: 'Something went wrong. Please try again.', [ErrorCodes.API_ERROR]: 'An error occurred. Please try again.', [ErrorCodes.PARSE_ERROR]: 'Failed to process response. Please try again.', [ErrorCodes.EXCEPTION]: 'An unexpected error occurred.', [ErrorCodes.NOT_AUTHENTICATED]: 'Please log in to continue.', [ErrorCodes.NO_BENEFICIARY_SELECTED]: 'Please select a beneficiary.', [ErrorCodes.NO_DEPLOYMENT]: 'No deployment configured.', }; // Action hints for error codes const actionHints: Record = { [ErrorCodes.NETWORK_ERROR]: 'Check your Wi-Fi or cellular connection', [ErrorCodes.NETWORK_OFFLINE]: 'Connect to the internet to continue', [ErrorCodes.UNAUTHORIZED]: 'Tap here to log in again', [ErrorCodes.TOKEN_EXPIRED]: 'Tap here to log in again', [ErrorCodes.BLE_NOT_ENABLED]: 'Enable Bluetooth in Settings', [ErrorCodes.BLE_PERMISSION_DENIED]: 'Grant Bluetooth permission in Settings', [ErrorCodes.SUBSCRIPTION_REQUIRED]: 'View subscription options', [ErrorCodes.SUBSCRIPTION_EXPIRED]: 'Renew your subscription', [ErrorCodes.PAYMENT_FAILED]: 'Try a different payment method', }; // Classify HTTP status to error category function classifyHttpStatus(status: number): ErrorCategory { if (status === 401) return 'authentication'; if (status === 403) return 'permission'; if (status === 404) return 'notFound'; if (status === 409) return 'conflict'; if (status === 429) return 'rateLimit'; if (status === 422 || status === 400) return 'validation'; if (status >= 500) return 'server'; if (status >= 400) return 'client'; return 'unknown'; } // Classify error code to category function classifyErrorCode(code: string): ErrorCategory { // Check timeout first before network (NETWORK_TIMEOUT should be timeout, not network) if (code.includes('TIMEOUT')) return 'timeout'; if (code.startsWith('NETWORK') || code === 'NETWORK_ERROR') return 'network'; if (code.includes('AUTH') || code.includes('TOKEN') || code.includes('SESSION') || code === 'UNAUTHORIZED') { return 'authentication'; } if (code.includes('PERMISSION') || code === 'FORBIDDEN') return 'permission'; if (code.includes('NOT_FOUND')) return 'notFound'; if (code.includes('CONFLICT') || code.includes('DUPLICATE') || code.includes('MISMATCH')) return 'conflict'; if (code.includes('VALIDATION') || code.includes('INVALID') || code.includes('MISSING')) return 'validation'; if (code.includes('RATE') || code.includes('TOO_MANY')) return 'rateLimit'; if (code.includes('SERVER') || code.includes('UNAVAILABLE') || code.includes('MAINTENANCE')) return 'server'; if (code.startsWith('BLE')) return 'ble'; if (code.startsWith('SENSOR')) return 'sensor'; if (code.includes('SUBSCRIPTION') || code.includes('PAYMENT')) return 'subscription'; return 'unknown'; } // Determine severity from category and status function determineSeverity(category: ErrorCategory, status?: number): ErrorSeverity { // Critical - app cannot continue if (category === 'authentication') return 'error'; if (status && status >= 500) return 'error'; if (category === 'server') return 'error'; // Warning - recoverable if (category === 'network' || category === 'timeout') return 'warning'; if (category === 'rateLimit') return 'warning'; // Error - operation failed if (category === 'validation' || category === 'permission' || category === 'notFound') return 'error'; // Default to error return 'error'; } // Get user message for error function getUserMessage(code: string, message?: string): string { // Use predefined message if available if (userMessages[code]) { return userMessages[code]; } // Fall back to provided message or generic return message || 'Something went wrong. Please try again.'; } // Get action hint for error function getActionHint(code: string): string | undefined { return actionHints[code]; } // Get retry policy for category function getRetryPolicy(category: ErrorCategory): RetryPolicy { return DefaultRetryPolicies[category] || DefaultRetryPolicies.unknown; } /** * Create AppError from API error */ export function createErrorFromApi(apiError: ApiError, context?: Record): AppError { const code = apiError.code || ErrorCodes.API_ERROR; const status = apiError.status; const category = status ? classifyHttpStatus(status) : classifyErrorCode(code); const severity = determineSeverity(category, status); return { message: apiError.message, code, severity, category, status, retry: getRetryPolicy(category), userMessage: getUserMessage(code, apiError.message), actionHint: getActionHint(code), context, timestamp: new Date(), errorId: generateErrorId(), }; } /** * Create AppError from JavaScript Error */ export function createErrorFromException( error: Error, code: string = ErrorCodes.EXCEPTION, context?: Record ): AppError { const category = classifyErrorCode(code); const severity = determineSeverity(category); return { message: error.message, code, severity, category, retry: getRetryPolicy(category), userMessage: getUserMessage(code, error.message), actionHint: getActionHint(code), context, timestamp: new Date(), errorId: generateErrorId(), originalError: error, }; } /** * Create AppError from network error */ export function createNetworkError( message: string = 'Network request failed', isTimeout: boolean = false ): AppError { const code = isTimeout ? ErrorCodes.NETWORK_TIMEOUT : ErrorCodes.NETWORK_ERROR; const category: ErrorCategory = isTimeout ? 'timeout' : 'network'; return { message, code, severity: 'warning', category, retry: getRetryPolicy(category), userMessage: getUserMessage(code), actionHint: getActionHint(code), timestamp: new Date(), errorId: generateErrorId(), }; } /** * Create AppError for validation with field errors */ export function createValidationError( message: string, fieldErrors: FieldError[] = [] ): AppError { return { message, code: ErrorCodes.VALIDATION_ERROR, severity: 'error', category: 'validation', retry: { isRetryable: false }, userMessage: getUserMessage(ErrorCodes.VALIDATION_ERROR, message), fieldErrors, timestamp: new Date(), errorId: generateErrorId(), }; } /** * Create AppError from any unknown error */ export function createErrorFromUnknown( error: unknown, defaultCode: string = ErrorCodes.UNKNOWN_ERROR, context?: Record ): AppError { // Already an AppError if (error && typeof error === 'object' && 'errorId' in error) { return error as AppError; } // JavaScript Error if (error instanceof Error) { return createErrorFromException(error, defaultCode, context); } // ApiError-like object if (error && typeof error === 'object' && 'message' in error) { return createErrorFromApi(error as ApiError, context); } // String error if (typeof error === 'string') { return createErrorFromException(new Error(error), defaultCode, context); } // Unknown return { message: 'An unknown error occurred', code: defaultCode, severity: 'error', category: 'unknown', retry: { isRetryable: false }, userMessage: getUserMessage(defaultCode), timestamp: new Date(), errorId: generateErrorId(), originalError: error, context, }; } /** * Create a simple error with predefined code */ export function createError( code: ErrorCode, message?: string, context?: Record ): AppError { const category = classifyErrorCode(code); const severity = determineSeverity(category); return { message: message || getUserMessage(code), code, severity, category, retry: getRetryPolicy(category), userMessage: getUserMessage(code, message), actionHint: getActionHint(code), context, timestamp: new Date(), errorId: generateErrorId(), }; } /** * Calculate retry delay with optional exponential backoff */ export function calculateRetryDelay(policy: RetryPolicy, attempt: number): number { if (!policy.retryDelayMs) return 1000; if (policy.useExponentialBackoff) { // Exponential backoff: delay * 2^attempt with jitter const baseDelay = policy.retryDelayMs * Math.pow(2, attempt); const jitter = Math.random() * 0.3 * baseDelay; // 0-30% jitter return Math.min(baseDelay + jitter, 30000); // Max 30 seconds } return policy.retryDelayMs; } /** * Log error for debugging/analytics (placeholder for future integration) */ export function logError(error: AppError): void { if (__DEV__) { console.error(`[${error.severity.toUpperCase()}] ${error.code}:`, error.message, { category: error.category, errorId: error.errorId, context: error.context, }); } // TODO: Integrate with error tracking service (e.g., Sentry, Bugsnag) } // Export error codes for convenience export { ErrorCodes };