/** * Enhanced Error Utilities for GridPilot * * Provides comprehensive error handling, validation, and user-friendly error messages * for both end users and developers. */ import { ApiError } from '@/lib/api/base/ApiError'; export interface ValidationError { field: string; message: string; value?: unknown; } export interface EnhancedErrorContext { timestamp?: string; component?: string; action?: string; formData?: Record; userId?: string; sessionId?: string; } /** * Parse API error response to extract validation errors or user-friendly messages */ export function parseApiError(error: unknown): { userMessage: string; developerMessage: string; validationErrors: ValidationError[]; isValidationError: boolean; } { const result = { userMessage: 'An unexpected error occurred', developerMessage: '', validationErrors: [] as ValidationError[], isValidationError: false, }; if (error instanceof ApiError) { result.developerMessage = error.getDeveloperMessage(); // Check if it's a validation error if (error.type === 'VALIDATION_ERROR') { result.isValidationError = true; result.userMessage = 'Please check your input and try again'; // Try to parse validation details from response try { if (error.context.responseText) { const parsed = JSON.parse(error.context.responseText); // Handle NestJS validation error format if (parsed.message && Array.isArray(parsed.message)) { result.validationErrors = parsed.message.map((msg: unknown) => { const m = msg as { property?: string; field?: string; constraints?: Record; message?: string; value?: unknown }; return { field: m.property || m.field || 'unknown', message: m.constraints ? Object.values(m.constraints).join(', ') : m.message || 'Invalid value', value: m.value, }; }); } // Handle custom error format else if (parsed.errors && Array.isArray(parsed.errors)) { result.validationErrors = parsed.errors.map((err: unknown) => { const e = err as { field?: string; property?: string; message?: string; value?: unknown }; return { field: e.field || e.property || 'unknown', message: e.message || 'Invalid value', value: e.value, }; }); } // Handle single message else if (parsed.message && typeof parsed.message === 'string') { result.userMessage = parsed.message; } } } catch (e) { // If parsing fails, use default messages } } else { result.userMessage = error.getUserMessage(); } } else if (error instanceof Error) { result.userMessage = error.message; result.developerMessage = error.message; } else { result.userMessage = 'An unknown error occurred'; result.developerMessage = String(error); } return result; } /** * Format validation errors for display in forms */ export function formatValidationErrorsForForm( validationErrors: ValidationError[] ): Record { const formErrors: Record = {}; validationErrors.forEach((error) => { // Map API field names to form field names const fieldName = mapApiFieldToFormField(error.field); formErrors[fieldName] = error.message; }); return formErrors; } /** * Map API field names to form field names */ function mapApiFieldToFormField(apiField: string): string { const fieldMap: Record = { 'rememberMe': 'rememberMe', 'email': 'email', 'password': 'password', 'displayName': 'displayName', 'firstName': 'firstName', 'lastName': 'lastName', 'confirmPassword': 'confirmPassword', 'role': 'role', }; return fieldMap[apiField] || apiField; } /** * Create enhanced error context for debugging */ export function createErrorContext( error: unknown, context: EnhancedErrorContext ): EnhancedErrorContext { return { timestamp: new Date().toISOString(), ...context, }; } /** * Check if error is retryable */ export function isRetryable(error: unknown): boolean { if (error instanceof ApiError) { return error.isRetryable(); } return false; } /** * Check if error is a network connectivity issue */ export function isConnectivityError(error: unknown): boolean { if (error instanceof ApiError) { return error.isConnectivityIssue(); } return false; } /** * Get error severity for logging and display */ export function getErrorSeverity(error: unknown): 'error' | 'warning' | 'info' { if (error instanceof ApiError) { const severity = error.getSeverity(); if (severity === 'error') return 'error'; if (severity === 'warn') return 'warning'; return 'info'; } return 'error'; } /** * Create user-friendly error summary */ export function createUserErrorSummary(error: unknown): { title: string; description: string; action: string; } { const parsed = parseApiError(error); if (parsed.isValidationError) { return { title: 'Invalid Input', description: parsed.userMessage, action: 'Please review your input and try again', }; } if (isConnectivityError(error)) { return { title: 'Connection Issue', description: 'Unable to connect to the server', action: 'Check your internet connection and try again', }; } if (isRetryable(error)) { return { title: 'Temporary Issue', description: parsed.userMessage, action: 'Please try again in a moment', }; } return { title: 'Error', description: parsed.userMessage, action: 'Please try again or contact support if the issue persists', }; } /** * Log error with context (only in development) */ export function logErrorWithContext( error: unknown, context: EnhancedErrorContext ): void { if (process.env.NODE_ENV !== 'development') return; const parsed = parseApiError(error); const severity = getErrorSeverity(error); console.group(`🚨 [${severity.toUpperCase()}] ${context.component || 'Unknown'} - ${context.action || 'Unknown action'}`); console.error('User Message:', parsed.userMessage); console.error('Developer Message:', parsed.developerMessage); if (parsed.validationErrors.length > 0) { console.error('Validation Errors:', parsed.validationErrors); } if (context.formData) { console.error('Form Data:', context.formData); } console.error('Context:', context); console.error('Original Error:', error); console.groupEnd(); } /** * Delay execution for retry logic */ export async function delay(ms: number): Promise { await new Promise(resolve => setTimeout(resolve, ms)); } /** * Retry operation with exponential backoff */ export async function retryWithBackoff( operation: () => Promise, maxRetries: number = 3, baseDelay: number = 1000 ): Promise { let lastError: unknown; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await operation(); } catch (error) { lastError = error; if (attempt === maxRetries || !isRetryable(error)) { throw error; } // Exponential backoff: 1s, 2s, 4s const delayMs = baseDelay * Math.pow(2, attempt); await delay(delayMs); } } throw lastError; }