/** * Error Handling Utilities * * Centralized error handling functions to eliminate code duplication */ export interface ErrorWithContext { error: Error; context: Record; } /** * Standardized error creation with context */ export function createError(message: string, context: Record = {}): ErrorWithContext { return { error: new Error(message), context }; } /** * Safe error message extraction */ export function extractErrorMessage(error: unknown): string { if (error instanceof Error) { return error.message; } if (typeof error === 'string') { return error; } if (error && typeof error === 'object' && 'message' in error) { return String(error.message); } return 'Unknown error'; } /** * Safe error stack extraction */ export function extractErrorStack(error: unknown): string | undefined { if (error instanceof Error && error.stack) { return error.stack; } return undefined; } /** * Convert unknown error to Error instance */ export function normalizeError(error: unknown, defaultMessage: string = 'Unknown error'): Error { if (error instanceof Error) { return error; } const message = extractErrorMessage(error); return new Error(message || defaultMessage); } /** * Retry with exponential backoff utility */ export interface RetryConfig { maxAttempts: number; delay: number; backoff: 'linear' | 'exponential'; jitter?: boolean; } export async function retryWithBackoff( operation: (attempt: number) => Promise, config: RetryConfig ): Promise { let lastError: Error = new Error('No attempts made'); for (let attempt = 1; attempt <= config.maxAttempts; attempt++) { try { return await operation(attempt); } catch (error) { lastError = normalizeError(error); if (attempt === config.maxAttempts) { throw lastError; } const delay = calculateDelay(attempt, config); await sleep(delay); } } throw lastError; } /** * Calculate retry delay with backoff */ function calculateDelay(attempt: number, config: RetryConfig): number { let delay = config.delay; if (config.backoff === 'exponential') { delay = config.delay * Math.pow(2, attempt - 1); } else { delay = config.delay * attempt; } // Add jitter to prevent thundering herd if (config.jitter) { delay = delay * (0.5 + Math.random() * 0.5); } return Math.min(delay, 30000); // Cap at 30 seconds } /** * Promise-based sleep */ function sleep(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Create timeout wrapper for promises */ export function withTimeout(promise: Promise, timeoutMs: number): Promise { return Promise.race([ promise, new Promise((_, reject) => setTimeout(() => reject(new Error(`Operation timed out after ${timeoutMs}ms`)), timeoutMs) ) ]); } /** * Safe async operation wrapper */ export async function safeAsync( operation: () => Promise, fallback: T, onError?: (error: Error) => void ): Promise { try { return await operation(); } catch (error) { const normalizedError = normalizeError(error); if (onError) { onError(normalizedError); } return fallback; } } /** * Environment detection utilities */ export function isBrowser(): boolean { return typeof window !== 'undefined' && typeof window.document !== 'undefined'; } export function isNode(): boolean { return typeof process !== 'undefined' && process.versions !== undefined && process.versions.node !== undefined; } /** * Browser/Node.js feature detection */ export function hasFeature(feature: 'fetch' | 'WebSocket' | 'localStorage' | 'crypto'): boolean { switch (feature) { case 'fetch': return typeof fetch !== 'undefined'; case 'WebSocket': return typeof WebSocket !== 'undefined'; case 'localStorage': try { return typeof localStorage !== 'undefined' && localStorage !== null; } catch { return false; } case 'crypto': return (typeof crypto !== 'undefined') || (typeof globalThis !== 'undefined' && typeof globalThis.crypto !== 'undefined'); default: return false; } }