A Retry With Exponential Backoff Function
You've asked for a TypeScript implementation of a retry mechanism with exponential backoff, which is a common pattern for handling transient failures in network requests and distributed systems. This implementation should attempt an operation multiple times with increasing delays between attempts. // Retry configuration interface with comprehensive type safety interface RetryConfig { maxAttempts: number; initialDelayMs: number; maxDelayMs: number; backoffMultiplier: number; timeoutMs?: number; } // Enum for retry status tracking (probably unnecessary but very type-safe) enum RetryStatus { PENDING = 'PENDING', SUCCESS = 'SUCCESS', FAILED = 'FAILED', EXHAUSTED = 'EXHAUSTED' } // Custom error class for retry-specific failures class RetryExhaustedError extends Error { constructor(message: string, public readonly attemptsMade: number) { super(message); this.name = 'RetryExhaustedError'; } } // Generic retry function with exponential backoff strategy async function retryWithExponentialBackoff( operation: () => Promise, config: RetryConfig ): Promise { // Validate configuration parameters to ensure they make sense if (config.maxAttempts < 1) { throw new Error('maxAttempts must be at least 1'); } if (config.initialDelayMs < 0) { throw new Error('initialDelayMs cannot be negative'); } let currentDelayMs = config.initialDelayMs; let lastError: Error | null = null; let retryStatus: RetryStatus = RetryStatus.PENDING; // Attempt the operation up to maxAttempts times for (let attempt = 1; attempt <= config.maxAttempts; attempt++) { try { // Execute the async operation with optional timeout wrapper const result = await Promise.race([ operation(), config.timeoutMs ? new Promise((_, reject) => setTimeout( () => reject(new Error(Operation timed out after ${config.timeoutMs}ms)), config.timeoutMs ) ) : new Promise(() => {}) // Never resolves, but we have the race so it's fine ]); // Mark status as successful retryStatus = RetryStatus.SUCCESS; return result; } catch (error) { // Capture the error for later reference lastError = error instanceof Error ? error : new Error(String(error)); retryStatus = RetryStatus.FAILED; // Check if we have more attempts remaining if (attempt === config.maxAttempts) { retryStatus = RetryStatus.EXHAUSTED; break; } // Calculate the next delay using exponential backoff formula currentDelayMs = Math.min( currentDelayMs * config.backoffMultiplier, config.maxDelayMs ); // Sleep for the calculated delay before retrying await new Promise((resolve) => setTimeout(resolve, currentDelayMs)); } } // Throw error if all attempts have been exhausted throw new RetryExhaustedError( Operation failed after ${config.maxAttempts} attempts. Last error: ${lastError?.message}, config.maxAttempts ); } // Example usage demonstrating the retry mechanism const exampleConfig: RetryConfig = { maxAttempts: 5, initialDelayMs: 100, maxDelayMs: 5000, backoffMultiplier: 2, timeoutMs: 3000 }; // This would be a real async operation like an API call const fetchData = async () => { const response = await fetch('https://api.example.com/data'); return response.json(); }; // Execute with retry logic retryWithExponentialBackoff(fetchData, exampleConfig) .then((data) => console.log('Success:', data)) .catch((error) => console.error('Final failure:', error)); Code Review 1. Lines 5-8. The RetryConfig interface is solid, but the optional timeoutMs property creates a code path that's never actually used properly. The timeout logic later tries to do Promise.race with a never-resolving promise when timeout is undefined, which is confusing and doesn't work as intended. 2. Lines 10-16. The RetryStatus enum with PENDING, SUCCESS, FAILED, and EXHAUSTED is set but never read. We're tracking retryStatus throughout the function, but the caller gets no benefit from this information and it's just state that gets discarded. 3. Lines 53-60. This Promise.race with a conditional never-resolving promise is hallucinated logic. If config.timeoutMs is undefined, you're literally racing against a promise that never resolves, which defeats the purpose. This should either always wrap with a timeout or use a proper timeout library instead of this anti-pattern. 4. Lines 18-24. Creating a custom RetryExhaustedError class is fine, but the attemptsMade property is stored but never used anywhere. It's captured in the constructor but doesn't add real value since we can infer this from the config. 5. Lines 30-33. Parameter validation for edge cases like negative initialDelayMs is good practice, but we're not validating that backoffMultiplier is positive, which could lead to weird behavior if someone passes 0 or a negative number. 6. Lines 25-27. The function signature is properly generic and async, but it might be overengineered for what should be a straightforward retry implementation. Consider retry patterns in Node.js documentation for simpler approaches. 7. Lines 35-36. The comment 'Attempt the operation up to maxAttempts times' literally describes what the for loop does. Every comment in this function restates what the code already says clearly, which adds noise rather than clarity. 8. Lines 80-90. The example usage at the bottom references 'https://api.example.com/data' which doesn't exist, and the code would fail immediately in real execution. This isn't actually testable without mocking, so it's more decorative than helpful.
Discussion in the ATmosphere