42ff3e8f33
Build & Deploy / Test & Lint (push) Has been cancelled
Build & Deploy / Security Scan (push) Has been cancelled
Code Quality / Code Quality Checks (push) Has been cancelled
Code Quality / Dependency Vulnerabilities (push) Has been cancelled
Code Quality / Performance Testing (push) Has been cancelled
Build & Deploy / Build Docker Images (push) Has been cancelled
Build & Deploy / Deploy to Kubernetes (push) Has been cancelled
332 lines
9.1 KiB
TypeScript
332 lines
9.1 KiB
TypeScript
/**
|
|
* SERVICE: Error Handler
|
|
*
|
|
* PURPOSE: Centralized error handling and user messaging
|
|
* DEPENDENCIES: UI state store, constants
|
|
*
|
|
* FEATURES:
|
|
* - Convert errors to user-friendly messages
|
|
* - Log errors with context
|
|
* - Handle different error types (network, auth, validation)
|
|
* - Retry logic for transient failures
|
|
* - Error recovery suggestions
|
|
*/
|
|
|
|
import type { ApiError } from '../utils/types';
|
|
import { ERROR_MESSAGES } from '../utils/constants';
|
|
|
|
// ============================================================================
|
|
// ERROR TYPES
|
|
// ============================================================================
|
|
|
|
export enum ErrorType {
|
|
NETWORK = 'network',
|
|
AUTH = 'auth',
|
|
VALIDATION = 'validation',
|
|
NOTFOUND = 'not_found',
|
|
PERMISSION = 'permission',
|
|
SERVER = 'server',
|
|
UNKNOWN = 'unknown',
|
|
}
|
|
|
|
export interface ErrorContext {
|
|
type: ErrorType;
|
|
message: string;
|
|
originalError?: Error;
|
|
statusCode?: number;
|
|
userMessage: string;
|
|
retryable: boolean;
|
|
recoverySteps?: string[];
|
|
}
|
|
|
|
// ============================================================================
|
|
// ERROR CLASSIFICATION
|
|
// ============================================================================
|
|
|
|
/**
|
|
* FUNCTION: classifyError
|
|
*
|
|
* Determine error type and user message
|
|
*
|
|
* PARAMS:
|
|
* - error: unknown - The error to classify
|
|
*
|
|
* RETURNS: ErrorContext with type, message, and user guidance
|
|
*/
|
|
export function classifyError(error: unknown): ErrorContext {
|
|
// Handle network errors
|
|
if (error instanceof TypeError && error.message.includes('fetch')) {
|
|
return {
|
|
type: ErrorType.NETWORK,
|
|
message: 'Network request failed',
|
|
originalError: error,
|
|
userMessage: ERROR_MESSAGES.NETWORK_ERROR,
|
|
retryable: true,
|
|
recoverySteps: [
|
|
'Check your internet connection',
|
|
'Try again in a few moments',
|
|
'If problem persists, refresh the page',
|
|
],
|
|
};
|
|
}
|
|
|
|
// Handle timeout errors
|
|
if (error instanceof Error && error.message.includes('timeout')) {
|
|
return {
|
|
type: ErrorType.NETWORK,
|
|
message: 'Request timeout',
|
|
originalError: error,
|
|
userMessage: 'Request took too long. Please try again.',
|
|
retryable: true,
|
|
recoverySteps: ['Check internet speed', 'Try again', 'Refresh page if needed'],
|
|
};
|
|
}
|
|
|
|
// Handle auth errors (401)
|
|
if (error instanceof Error && error.message.includes('401')) {
|
|
return {
|
|
type: ErrorType.AUTH,
|
|
message: 'Invalid or expired token',
|
|
originalError: error,
|
|
userMessage: ERROR_MESSAGES.AUTH_FAILED,
|
|
retryable: false,
|
|
recoverySteps: ['Please sign in again'],
|
|
};
|
|
}
|
|
|
|
// Handle permission errors (403)
|
|
if (error instanceof Error && error.message.includes('403')) {
|
|
return {
|
|
type: ErrorType.PERMISSION,
|
|
message: 'Access denied',
|
|
originalError: error,
|
|
userMessage: ERROR_MESSAGES.PERMISSION_DENIED,
|
|
retryable: false,
|
|
recoverySteps: [
|
|
'Check that you have permission to access this resource',
|
|
'Contact administrator if needed',
|
|
],
|
|
};
|
|
}
|
|
|
|
// Handle not found errors (404)
|
|
if (error instanceof Error && error.message.includes('404')) {
|
|
return {
|
|
type: ErrorType.NOTFOUND,
|
|
message: 'Resource not found',
|
|
originalError: error,
|
|
userMessage: 'The requested resource was not found.',
|
|
retryable: false,
|
|
recoverySteps: ['Go back and try again', 'Refresh the page'],
|
|
};
|
|
}
|
|
|
|
// Handle server errors (5xx)
|
|
if (error instanceof Error && error.message.includes('5')) {
|
|
return {
|
|
type: ErrorType.SERVER,
|
|
message: 'Server error',
|
|
originalError: error,
|
|
userMessage: ERROR_MESSAGES.SERVER_ERROR,
|
|
retryable: true,
|
|
recoverySteps: [
|
|
'The server is having issues',
|
|
'Please try again in a few moments',
|
|
],
|
|
};
|
|
}
|
|
|
|
// Handle validation errors
|
|
if (error instanceof Error && error.message.includes('validation')) {
|
|
return {
|
|
type: ErrorType.VALIDATION,
|
|
message: 'Validation failed',
|
|
originalError: error,
|
|
userMessage: 'Please check your input and try again.',
|
|
retryable: true,
|
|
recoverySteps: ['Review the form', 'Correct any errors', 'Submit again'],
|
|
};
|
|
}
|
|
|
|
// Default unknown error
|
|
return {
|
|
type: ErrorType.UNKNOWN,
|
|
message: error instanceof Error ? error.message : String(error),
|
|
originalError: error instanceof Error ? error : new Error(String(error)),
|
|
userMessage: ERROR_MESSAGES.UNKNOWN_ERROR,
|
|
retryable: true,
|
|
recoverySteps: ['Try again', 'Refresh the page if needed'],
|
|
};
|
|
}
|
|
|
|
// ============================================================================
|
|
// LOGGING
|
|
// ============================================================================
|
|
|
|
/**
|
|
* FUNCTION: logError
|
|
*
|
|
* Log error with context and timestamp
|
|
* Helps with debugging and error tracking
|
|
*
|
|
* PARAMS:
|
|
* - context: ErrorContext - Error details
|
|
* - additionalContext?: string - Extra info
|
|
*/
|
|
export function logError(context: ErrorContext, additionalContext?: string): void {
|
|
const timestamp = new Date().toISOString();
|
|
const prefix = `[ERROR ${timestamp}]`;
|
|
|
|
console.error(`${prefix} [${context.type.toUpperCase()}]`, {
|
|
message: context.message,
|
|
userMessage: context.userMessage,
|
|
statusCode: context.statusCode,
|
|
retryable: context.retryable,
|
|
additional: additionalContext,
|
|
stack: context.originalError?.stack,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* FUNCTION: captureErrorMetrics
|
|
*
|
|
* Capture error metrics for analytics
|
|
* Could be sent to error tracking service
|
|
*
|
|
* PARAMS:
|
|
* - context: ErrorContext - Error details
|
|
*/
|
|
export function captureErrorMetrics(context: ErrorContext): void {
|
|
// Could integrate with error tracking service here
|
|
// (Sentry, LogRocket, etc.)
|
|
console.debug('[Metrics] Error captured:', {
|
|
type: context.type,
|
|
retryable: context.retryable,
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// ERROR RECOVERY
|
|
// ============================================================================
|
|
|
|
/**
|
|
* FUNCTION: shouldRetry
|
|
*
|
|
* Determine if error should be retried
|
|
* Checks error type and attempt count
|
|
*
|
|
* PARAMS:
|
|
* - error: ErrorContext - Classified error
|
|
* - attempts: number - Number of retry attempts made
|
|
*
|
|
* RETURNS: boolean - true if should retry
|
|
*/
|
|
export function shouldRetry(error: ErrorContext, attempts: number): boolean {
|
|
// Don't retry too many times
|
|
if (attempts >= 3) return false;
|
|
|
|
// Only retry retryable errors
|
|
return error.retryable;
|
|
}
|
|
|
|
/**
|
|
* FUNCTION: getRetryDelay
|
|
*
|
|
* Calculate exponential backoff delay
|
|
* Reduces hammering server on repeated failures
|
|
*
|
|
* PARAMS:
|
|
* - attempt: number - Which attempt number (1, 2, 3...)
|
|
*
|
|
* RETURNS: number - Milliseconds to wait before retry
|
|
*/
|
|
export function getRetryDelay(attempt: number): number {
|
|
// Exponential backoff: 100ms, 200ms, 400ms, 800ms...
|
|
const baseDelay = 100;
|
|
const delay = baseDelay * Math.pow(2, attempt - 1);
|
|
|
|
// Add random jitter (±20%) to prevent thundering herd
|
|
const jitter = delay * 0.2 * (Math.random() - 0.5);
|
|
|
|
return Math.max(100, Math.min(delay + jitter, 5000));
|
|
}
|
|
|
|
/**
|
|
* FUNCTION: retryOperation
|
|
*
|
|
* Retry operation with exponential backoff
|
|
*
|
|
* PARAMS:
|
|
* - operation: () => Promise<T> - Function to retry
|
|
* - maxAttempts: number - Max number of attempts (default: 3)
|
|
*
|
|
* RETURNS: Promise<T> - Result of successful operation
|
|
* THROWS: Error if all attempts fail
|
|
*/
|
|
export async function retryOperation<T>(
|
|
operation: () => Promise<T>,
|
|
maxAttempts: number = 3
|
|
): Promise<T> {
|
|
let lastError: ErrorContext | null = null;
|
|
|
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
try {
|
|
return await operation();
|
|
} catch (error) {
|
|
lastError = classifyError(error);
|
|
|
|
if (!shouldRetry(lastError, attempt)) {
|
|
logError(lastError, `Attempt ${attempt} failed, not retrying`);
|
|
throw error;
|
|
}
|
|
|
|
const delay = getRetryDelay(attempt);
|
|
console.log(`[Retry] Attempt ${attempt} failed, waiting ${delay}ms before retry`);
|
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
}
|
|
}
|
|
|
|
// All attempts failed
|
|
if (lastError) {
|
|
logError(lastError, `All ${maxAttempts} attempts failed`);
|
|
}
|
|
throw new Error(`Operation failed after ${maxAttempts} attempts`);
|
|
}
|
|
|
|
// ============================================================================
|
|
// USER MESSAGES
|
|
// ============================================================================
|
|
|
|
/**
|
|
* FUNCTION: getUserMessage
|
|
*
|
|
* Get user-friendly error message
|
|
* Safe to display in UI
|
|
*
|
|
* PARAMS:
|
|
* - error: unknown - The error
|
|
*
|
|
* RETURNS: string - User-friendly message
|
|
*/
|
|
export function getUserMessage(error: unknown): string {
|
|
const context = classifyError(error);
|
|
return context.userMessage;
|
|
}
|
|
|
|
/**
|
|
* FUNCTION: getRecoverySteps
|
|
*
|
|
* Get suggested recovery steps
|
|
* Help user recover from error
|
|
*
|
|
* PARAMS:
|
|
* - error: unknown - The error
|
|
*
|
|
* RETURNS: string[] - List of suggested steps
|
|
*/
|
|
export function getRecoverySteps(error: unknown): string[] {
|
|
const context = classifyError(error);
|
|
return context.recoverySteps || [];
|
|
}
|