import { withTimeout as withTimeoutRace } from '../performance/timeout-wrapper.js'; import { TextProcessingError } from './text-error.js'; export interface ErrorHandlerOptions { maxRetries?: number; retryDelay?: number; exponentialBackoff?: boolean; onError?: (error: Error) => void; // NO FALLBACKS - fail fast per CLAUDE.md throwOnFatal?: boolean; } export class ErrorHandler { private readonly retryCount = new Map(); constructor(private readonly options: ErrorHandlerOptions = {}) {} async handle(operation: () => Promise, context?: string): Promise { const maxRetries = this.options.maxRetries || 3; const retryKey = context || 'default'; let lastError: Error | undefined; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { const result = await operation(); this.retryCount.delete(retryKey); return result; } catch (error) { lastError = error as Error; if (this.options.onError) { this.options.onError(lastError); } if (this.isFatalError(lastError)) { if (this.options.throwOnFatal !== false) { throw lastError; } break; } if (attempt < maxRetries) { const delay = this.calculateDelay(attempt); await this.sleep(delay); this.retryCount.set(retryKey, attempt + 1); } } } // NO FALLBACKS - fail fast per CLAUDE.md throw lastError || new Error('Operation failed'); } handleSync(operation: () => T, _context?: string): T { try { return operation(); } catch (error) { if (this.options.onError) { this.options.onError(error as Error); } if (this.isFatalError(error as Error) && this.options.throwOnFatal !== false) { throw error; } // NO FALLBACKS - fail fast per CLAUDE.md throw error; } } private isFatalError(error: Error): boolean { // Determine if error is non-recoverable if (error instanceof TextProcessingError) { const fatalCodes = ['VALIDATION_ERROR', 'PARSE_ERROR', 'TEMPLATE_ERROR']; return fatalCodes.includes(error.code || ''); } // Check for common fatal errors return ( error.name === 'TypeError' || error.name === 'ReferenceError' || error.name === 'SyntaxError' ); } private calculateDelay(attempt: number): number { const baseDelay = this.options.retryDelay || 1000; if (this.options.exponentialBackoff) { return baseDelay * Math.pow(2, attempt); } return baseDelay; } private sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Generic function wrapper requires any wrap any>(fn: T, context?: string): T { return ((...args: Parameters) => { if (fn.constructor.name === 'AsyncFunction') { return this.handle(() => fn(...args), context); } return this.handleSync(() => fn(...args), context); }) as T; } static createRetryable(fn: () => Promise, options?: ErrorHandlerOptions): () => Promise { const handler = new ErrorHandler(options); return () => handler.handle(fn); } // REMOVED withFallback - NO FALLBACKS per CLAUDE.md // If operation fails, it should fail fast and loud static async withTimeout( fn: () => Promise, timeout: number, timeoutError?: Error, ): Promise { return withTimeoutRace( fn(), timeout, timeoutError?.message, ); } getRetryCount(context?: string): number { return this.retryCount.get(context || 'default') || 0; } resetRetryCount(context?: string): void { if (context) { this.retryCount.delete(context); } else { this.retryCount.clear(); } } } interface ErrorEntry { error: Error; context?: unknown; timestamp: Date; } // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Error constructor type requires any type ErrorConstructor = new (...args: any[]) => T; export class ErrorAggregator { private errors: ErrorEntry[] = []; add(error: Error, context?: unknown): void { this.errors.push({ error, context, timestamp: new Date(), }); } getErrors(): ErrorEntry[] { return [...this.errors]; } getErrorsByType(type: ErrorConstructor): T[] { return this.errors.filter((e) => e.error instanceof type).map((e) => e.error as T); } hasErrors(): boolean { return this.errors.length > 0; } hasErrorType(type: ErrorConstructor): boolean { return this.errors.some((e) => e.error instanceof type); } clear(): void { this.errors = []; } throwIfErrors(): void { if (this.errors.length === 0) { return; } if (this.errors.length === 1) { throw this.errors[0].error; } const aggregateError = new AggregateError( this.errors.map((e) => e.error), `${this.errors.length} errors occurred`, ); throw aggregateError; } getSummary(): string { const errorCounts = new Map(); for (const { error } of this.errors) { const name = error.name || 'Unknown'; errorCounts.set(name, (errorCounts.get(name) || 0) + 1); } const summary: string[] = []; for (const [name, count] of errorCounts) { summary.push(`${name}: ${count}`); } return summary.join(', '); } }