SOLUTION: Challenge 3 - Retry Logic ==================================== Challenge: Build a function that retries a failing async operation (simulated by a random success/failure) up to N times before giving up. --- SOLUTION: // Simulate an async operation that randomly fails async function unstableOperation(): Promise { return new Promise((resolve, reject) => { setTimeout(() => { if (Math.random() > 0.5) { resolve("Success!"); } else { reject(new Error("Random failure")); } }, 100); }); } // Retry wrapper with immediate retries async function retryWithImmediate( operation: () => Promise, maxRetries: number = 3 ): Promise { for (let attempt = 1; attempt <= maxRetries + 1; attempt++) { try { const result = await operation(); if (attempt > 1) { console.log(`✅ Success on attempt ${attempt}`); } return result; } catch (error) { if (attempt > maxRetries) { console.error(`❌ Failed after ${maxRetries} retries`); throw error; } console.warn(`Attempt ${attempt} failed, retrying...`); } } // Should never reach here throw new Error("Retry logic error"); } // Retry wrapper with exponential backoff async function retryWithBackoff( operation: () => Promise, maxRetries: number = 3, baseDelay: number = 100 ): Promise { for (let attempt = 1; attempt <= maxRetries + 1; attempt++) { try { const result = await operation(); if (attempt > 1) { console.log(`✅ Success on attempt ${attempt}`); } return result; } catch (error) { if (attempt > maxRetries) { console.error(`❌ Failed after ${maxRetries} retries`); throw error; } // Calculate delay: baseDelay * 2^(attempt-1) const delay = baseDelay * Math.pow(2, attempt - 1); console.warn(`Attempt ${attempt} failed. Retrying in ${delay}ms...`); // Wait before next attempt await new Promise(resolve => setTimeout(resolve, delay)); } } throw new Error("Retry logic error"); } // Test with immediate retries console.log("Testing immediate retries:"); try { const result = await retryWithImmediate(unstableOperation, 3); console.log("Result:", result); } catch (error) { console.error("Gave up after retries"); } // Test with exponential backoff console.log("\nTesting with backoff:"); try { const result = await retryWithBackoff(unstableOperation, 3, 100); console.log("Result:", result); } catch (error) { console.error("Gave up after retries"); } --- EXPLANATION: IMMEDIATE RETRIES: The simple version retries immediately without waiting. Good for transient network blips that clear quickly. Loop: 1 → 2 → 3 → 4 (total 4 attempts max) Wait: none between attempts EXPONENTIAL BACKOFF: Wait longer between each retry: 100ms, 200ms, 400ms, ... Good for rate-limited or overloaded servers. Gives the server time to recover. Loop: 1 (wait 100ms) → 2 (wait 200ms) → 3 (wait 400ms) → 4 Math: delay = baseDelay * 2^(attempt - 1) attempt 1: 100 * 2^0 = 100 attempt 2: 100 * 2^1 = 200 attempt 3: 100 * 2^2 = 400 WHEN TO USE WHAT: - Immediate retries: Client errors, temporary DNS issues, network blips - Exponential backoff: Server errors (overload, rate limiting), database locks - For production, combine: immediate retry once or twice, then backoff --- ADVANCED: Jitter (randomize backoff) To avoid thundering herd (all clients retrying at once), add jitter: async function retryWithJitter( operation: () => Promise, maxRetries: number = 3, baseDelay: number = 100 ): Promise { for (let attempt = 1; attempt <= maxRetries + 1; attempt++) { try { return await operation(); } catch (error) { if (attempt > maxRetries) throw error; // Exponential backoff + random jitter const baseWait = baseDelay * Math.pow(2, attempt - 1); const jitter = Math.random() * 0.1 * baseWait; // ±10% randomness const delay = baseWait + jitter; console.warn(`Retrying in ${Math.round(delay)}ms...`); await new Promise(resolve => setTimeout(resolve, delay)); } } throw new Error("Retry logic error"); } This prevents synchronized retries from multiple clients hammering the server. --- PRODUCTION PATTERN: async function robustFetch( url: string, options?: RequestInit, maxRetries: number = 3 ): Promise { let lastError: Error | null = null; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const response = await fetch(url, options); if (response.ok) { return await response.json(); } // Don't retry on 4xx errors (client fault) if (response.status >= 400 && response.status < 500) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } // For 5xx errors, retry throw new Error(`HTTP ${response.status}: ${response.statusText}`); } catch (error) { lastError = error as Error; if (attempt < maxRetries) { const delay = 100 * Math.pow(2, attempt - 1); await new Promise(resolve => setTimeout(resolve, delay)); } } } throw lastError || new Error("Unknown error"); } This respects HTTP semantics: don't retry client errors (4xx), only server/network errors. --- WHY RETRY PATTERNS MATTER: 1. Real networks fail. Code that never retries will mysteriously fail. 2. Transient errors (network hiccup, server busy) clear within milliseconds. 3. Exponential backoff prevents overwhelming a struggling server. 4. Jitter distributes load, preventing synchronized storms. 5. Better UX: app recovers gracefully instead of failing instantly.