Category: runtimeDifficulty: MediumPublished: 2024-12-18
Common Async/Await Pitfalls and Solutions
Async/await is a powerful feature in JavaScript for handling asynchronous operations, but it comes with its own set of common pitfalls and challenges. Understanding these issues is crucial for writing reliable asynchronous code.
Common Pitfalls
1. Forgetting to Await Promises
// Problem: Function returns before promise resolves async function fetchUserData(userId) { const userData = fetch(`/api/users/${userId}`); // Missing await console.log(userData); // Logs Promise object, not data return userData; } // Solution: Properly await promises async function fetchUserDataFixed(userId) { const response = await fetch(`/api/users/${userId}`); const userData = await response.json(); console.log(userData); // Logs actual data return userData; }
2. Error Handling Gaps
// Problem: Errors in async operations are not caught async function processData(data) { const result = await someAsyncOperation(data); return result; // Errors will propagate uncaught } // Solution: Proper error handling async function processDataFixed(data) { try { const result = await someAsyncOperation(data); return result; } catch (error) { console.error('Error processing data:', error); // Consider rethrowing or returning a default value throw new Error(`Failed to process data: ${error.message}`); } }
3. Promise.all Error Handling
// Problem: One failure stops all operations async function fetchAllUsers(userIds) { const users = await Promise.all( userIds.map(id => fetch(`/api/users/${id}`)) ); return users; } // Solution: Handle individual failures async function fetchAllUsersFixed(userIds) { const userPromises = userIds.map(async id => { try { const response = await fetch(`/api/users/${id}`); return await response.json(); } catch (error) { console.error(`Failed to fetch user ${id}:`, error); return null; // Or appropriate default value } }); const users = await Promise.all(userPromises); return users.filter(user => user !== null); }
Error Handling Patterns
1. Retry Pattern
async function fetchWithRetry(url, options = {}, maxRetries = 3) { let lastError; for (let attempt = 0; attempt < maxRetries; attempt++) { try { const response = await fetch(url, options); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.json(); } catch (error) { console.warn(`Attempt ${attempt + 1} failed:`, error); lastError = error; if (attempt < maxRetries - 1) { const delay = Math.pow(2, attempt) * 1000; // Exponential backoff await new Promise(resolve => setTimeout(resolve, delay)); } } } throw new Error(`Failed after ${maxRetries} attempts: ${lastError.message}`); } // Usage try { const data = await fetchWithRetry('/api/data'); console.log('Success:', data); } catch (error) { console.error('All retries failed:', error); }
2. Timeout Pattern
function timeout(promise, ms) { const timeoutPromise = new Promise((_, reject) => { setTimeout(() => { reject(new Error(`Operation timed out after ${ms}ms`)); }, ms); }); return Promise.race([promise, timeoutPromise]); } // Usage async function fetchWithTimeout(url, ms = 5000) { try { const response = await timeout(fetch(url), ms); return await response.json(); } catch (error) { if (error.message.includes('timed out')) { console.error('Request timed out'); } else { console.error('Request failed:', error); } throw error; } }
3. Circuit Breaker Pattern
class CircuitBreaker { constructor(fn, options = {}) { this.fn = fn; this.failures = 0; this.maxFailures = options.maxFailures || 3; this.resetTimeout = options.resetTimeout || 60000; this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN } async execute(...args) { if (this.state === 'OPEN') { throw new Error('Circuit breaker is OPEN'); } try { const result = await this.fn(...args); this.reset(); return result; } catch (error) { this.failures++; if (this.failures >= this.maxFailures) { this.tripBreaker(); } throw error; } } tripBreaker() { this.state = 'OPEN'; setTimeout(() => { this.state = 'HALF_OPEN'; }, this.resetTimeout); } reset() { this.failures = 0; this.state = 'CLOSED'; } } // Usage const breaker = new CircuitBreaker( async (url) => { const response = await fetch(url); return response.json(); }, { maxFailures: 3, resetTimeout: 10000 } ); async function fetchWithBreaker(url) { try { return await breaker.execute(url); } catch (error) { if (error.message === 'Circuit breaker is OPEN') { return { error: 'Service temporarily unavailable' }; } throw error; } }
Testing Async Code
describe('Async Operations', () => { it('should handle successful requests', async () => { const data = await fetchWithRetry('/api/test'); expect(data).toBeDefined(); }); it('should retry failed requests', async () => { // Mock failing request that succeeds on third try let attempts = 0; jest.spyOn(global, 'fetch').mockImplementation(() => { attempts++; if (attempts < 3) { return Promise.reject(new Error('Network error')); } return Promise.resolve(new Response('{"success": true}')); }); const data = await fetchWithRetry('/api/test'); expect(data.success).toBe(true); expect(attempts).toBe(3); }); it('should handle timeouts', async () => { await expect( fetchWithTimeout('/api/test', 100) ).rejects.toThrow('Operation timed out'); }); });
Best Practices
-
Always Chain Promises Properly
// Bad promise.then(result => { return saveToDatabase(result) }).then(saved => { return sendEmail(saved) }); // Good async function process() { const result = await promise; const saved = await saveToDatabase(result); return await sendEmail(saved); }
-
Handle All Error Cases
async function robustOperation() { try { const result = await riskyOperation(); return result; } catch (error) { if (error instanceof NetworkError) { // Handle network errors return await fallbackOperation(); } else if (error instanceof ValidationError) { // Handle validation errors return defaultValue; } else { // Handle unknown errors console.error('Unknown error:', error); throw error; } } }
-
Use Promise Methods Appropriately
// For parallel operations that all must succeed const results = await Promise.all([op1(), op2(), op3()]); // For parallel operations where some may fail const results = await Promise.allSettled([op1(), op2(), op3()]); // For operations where only the first success matters const result = await Promise.any([op1(), op2(), op3()]); // For operations where the first completion matters const result = await Promise.race([op1(), op2(), op3()]);
Performance Considerations
-
Parallel vs Sequential Execution
// Sequential (slower) const result1 = await operation1(); const result2 = await operation2(); // Parallel (faster) const [result1, result2] = await Promise.all([ operation1(), operation2() ]);
-
Caching Promises
class PromiseCache { constructor() { this.cache = new Map(); } async get(key, producer) { if (!this.cache.has(key)) { this.cache.set(key, producer()); } return this.cache.get(key); } invalidate(key) { this.cache.delete(key); } }
-
Resource Cleanup
async function withCleanup(resource, operation) { try { return await operation(resource); } finally { await resource.cleanup(); } }