/** * Unified Error Handling for Portal * * Clean, simple error handling that uses shared error codes from domain package. * Provides consistent error parsing and user-friendly messages. */ import { ApiError as ClientApiError, isApiError } from "@/lib/api"; import { ErrorCode, ErrorMessages, ErrorMetadata, matchErrorPattern, type ErrorCodeType, } from "@customer-portal/domain/common"; // ============================================================================ // Types // ============================================================================ export interface ParsedError { code: ErrorCodeType; message: string; shouldLogout: boolean; shouldRetry: boolean; } // ============================================================================ // Error Parsing // ============================================================================ /** * Parse any error into a structured format with code and user-friendly message. * This is the main entry point for error handling. */ export function parseError(error: unknown): ParsedError { // Handle API client errors if (isApiError(error)) { return parseApiError(error); } // Handle network/fetch errors if (error instanceof Error) { return parseNativeError(error); } // Handle string errors if (typeof error === "string") { const code = matchErrorPattern(error); const metadata = ErrorMetadata[code]; return { code, message: error, shouldLogout: metadata.shouldLogout, shouldRetry: metadata.shouldRetry, }; } // Unknown error type return { code: ErrorCode.UNKNOWN, message: ErrorMessages[ErrorCode.UNKNOWN], shouldLogout: false, shouldRetry: true, }; } /** * Parse API client error */ function parseApiError(error: ClientApiError): ParsedError { const body = error.body; const status = error.response?.status; // Try to extract from standard API error response format if (body && typeof body === "object") { const bodyObj = body as Record; // Check for standard { success: false, error: { code, message } } format if ( bodyObj.success === false && bodyObj.error && typeof bodyObj.error === "object" ) { const errorObj = bodyObj.error as Record; const code = typeof errorObj.code === "string" ? errorObj.code : undefined; const message = typeof errorObj.message === "string" ? errorObj.message : undefined; if (code && message) { const metadata = ErrorMetadata[code as ErrorCodeType] ?? ErrorMetadata[ErrorCode.UNKNOWN]; return { code: code as ErrorCodeType, message, shouldLogout: metadata.shouldLogout, shouldRetry: metadata.shouldRetry, }; } } // Try extracting message from body const extractedMessage = extractMessageFromBody(body); if (extractedMessage) { const code = matchErrorPattern(extractedMessage); const metadata = ErrorMetadata[code]; return { code, message: extractedMessage, shouldLogout: metadata.shouldLogout, shouldRetry: metadata.shouldRetry, }; } } // Fall back to status code mapping const code = mapStatusToErrorCode(status); const metadata = ErrorMetadata[code]; return { code, message: error.message || ErrorMessages[code], shouldLogout: metadata.shouldLogout, shouldRetry: metadata.shouldRetry, }; } /** * Parse native JavaScript errors (network, timeout, etc.) */ function parseNativeError(error: Error): ParsedError { // Network errors if (error.name === "TypeError" && error.message.includes("fetch")) { return { code: ErrorCode.NETWORK_ERROR, message: ErrorMessages[ErrorCode.NETWORK_ERROR], shouldLogout: false, shouldRetry: true, }; } // Timeout errors if (error.name === "AbortError") { return { code: ErrorCode.TIMEOUT, message: ErrorMessages[ErrorCode.TIMEOUT], shouldLogout: false, shouldRetry: true, }; } // Try pattern matching on error message const code = matchErrorPattern(error.message); const metadata = ErrorMetadata[code]; return { code, message: code === ErrorCode.UNKNOWN ? error.message : ErrorMessages[code], shouldLogout: metadata.shouldLogout, shouldRetry: metadata.shouldRetry, }; } /** * Extract error message from response body */ function extractMessageFromBody(body: unknown): string | null { if (!body || typeof body !== "object") { return null; } const bodyObj = body as Record; // Check nested error.message (standard format) if (bodyObj.error && typeof bodyObj.error === "object") { const errorObj = bodyObj.error as Record; if (typeof errorObj.message === "string") { return errorObj.message; } } // Check top-level message if (typeof bodyObj.message === "string") { return bodyObj.message; } return null; } /** * Map HTTP status code to error code */ function mapStatusToErrorCode(status?: number): ErrorCodeType { if (!status) return ErrorCode.UNKNOWN; switch (status) { case 400: return ErrorCode.VALIDATION_FAILED; case 401: return ErrorCode.SESSION_EXPIRED; case 403: return ErrorCode.FORBIDDEN; case 404: return ErrorCode.NOT_FOUND; case 409: return ErrorCode.ACCOUNT_EXISTS; case 429: return ErrorCode.RATE_LIMITED; case 503: return ErrorCode.SERVICE_UNAVAILABLE; default: return status >= 500 ? ErrorCode.INTERNAL_ERROR : ErrorCode.UNKNOWN; } } // ============================================================================ // Convenience Functions // ============================================================================ /** * Get user-friendly error message from any error */ export function getErrorMessage(error: unknown): string { return parseError(error).message; } /** * Check if error should trigger logout */ export function shouldLogout(error: unknown): boolean { return parseError(error).shouldLogout; } /** * Check if error can be retried */ export function canRetry(error: unknown): boolean { return parseError(error).shouldRetry; } /** * Get error code from any error */ export function getErrorCode(error: unknown): ErrorCodeType { return parseError(error).code; } // ============================================================================ // Re-exports from domain package for convenience // ============================================================================ export { ErrorCode, ErrorMessages, ErrorMetadata, matchErrorPattern, type ErrorCodeType, } from "@customer-portal/domain/common";