Understanding JavaScript Promises and async/await
Asynchronous programming is fundamental to JavaScript. Let's understand how Promises work and how async/await makes async code readable.
The Problem with Callbacks
javascript// Callback hell getUser(userId, (user) => { getOrders(user.id, (orders) => { getOrderDetails(orders[0].id, (details) => { getShippingInfo(details.shippingId, (shipping) => { console.log(shipping); // More nesting... }); }); }); });
This is hard to read, debug, and maintain.
Promises Basics
A Promise represents a value that will be available in the future.
Creating Promises
javascriptconst promise = new Promise((resolve, reject) => { // Async operation setTimeout(() => { const success = true; if (success) { resolve({ data: 'Success!' }); } else { reject(new Error('Something went wrong')); } }, 1000); });
Using Promises
javascriptpromise .then(result => { console.log(result.data); return processData(result); }) .then(processed => { console.log(processed); }) .catch(error => { console.error('Error:', error.message); }) .finally(() => { console.log('Cleanup'); });
Promise States
- Pending: Initial state, neither fulfilled nor rejected
- Fulfilled: Operation completed successfully
- Rejected: Operation failed
Promise Methods
Promise.all
Wait for all promises to resolve:
javascriptconst promises = [ fetch('/api/users'), fetch('/api/products'), fetch('/api/orders') ]; Promise.all(promises) .then(responses => Promise.all(responses.map(r => r.json()))) .then(([users, products, orders]) => { console.log(users, products, orders); }) .catch(error => { // Any rejection fails the whole thing console.error('One request failed:', error); });
Promise.allSettled
Get results of all promises, regardless of success/failure:
javascriptconst promises = [ fetch('/api/users'), fetch('/api/might-fail'), fetch('/api/products') ]; Promise.allSettled(promises) .then(results => { results.forEach((result, index) => { if (result.status === 'fulfilled') { console.log(`Promise ${index} succeeded:`, result.value); } else { console.log(`Promise ${index} failed:`, result.reason); } }); });
Promise.race
First promise to settle wins:
javascriptconst timeout = new Promise((_, reject) => { setTimeout(() => reject(new Error('Timeout')), 5000); }); Promise.race([fetch('/api/data'), timeout]) .then(response => response.json()) .catch(error => console.error(error.message));
Promise.any
First promise to fulfill wins:
javascriptconst mirrors = [ fetch('https://mirror1.example.com/data'), fetch('https://mirror2.example.com/data'), fetch('https://mirror3.example.com/data') ]; Promise.any(mirrors) .then(response => response.json()) .catch(error => { // AggregateError if all fail console.error('All mirrors failed'); });
async/await
Syntactic sugar over Promises that makes async code look synchronous.
Basic Usage
javascriptasync function fetchUserData(userId) { try { const response = await fetch(`/api/users/${userId}`); const user = await response.json(); return user; } catch (error) { console.error('Failed to fetch user:', error); throw error; } } // Using the async function fetchUserData(123) .then(user => console.log(user)) .catch(error => console.error(error));
Sequential vs Parallel
javascript// Sequential - slower async function sequential() { const user = await fetchUser(1); // Wait... const orders = await fetchOrders(1); // Then wait... const products = await fetchProducts(); // Then wait... return { user, orders, products }; } // Parallel - faster async function parallel() { const [user, orders, products] = await Promise.all([ fetchUser(1), fetchOrders(1), fetchProducts() ]); return { user, orders, products }; }
Error Handling
javascript// try/catch async function fetchWithRetry(url, retries = 3) { for (let i = 0; i < retries; i++) { try { const response = await fetch(url); if (!response.ok) throw new Error(`HTTP ${response.status}`); return await response.json(); } catch (error) { if (i === retries - 1) throw error; await delay(1000 * (i + 1)); // Exponential backoff } } } // .catch() on await async function fetchData() { const data = await fetch('/api/data') .then(r => r.json()) .catch(() => ({ fallback: true })); return data; }
Common Patterns
Async Iteration
javascriptasync function processItems(items) { for (const item of items) { await processItem(item); } } // Or with for-await-of for async iterables async function* asyncGenerator() { yield await fetchPage(1); yield await fetchPage(2); yield await fetchPage(3); } for await (const page of asyncGenerator()) { console.log(page); }
Concurrent Limit
javascriptasync function processWithLimit(items, limit, fn) { const results = []; const executing = []; for (const item of items) { const promise = fn(item).then(result => { executing.splice(executing.indexOf(promise), 1); return result; }); results.push(promise); executing.push(promise); if (executing.length >= limit) { await Promise.race(executing); } } return Promise.all(results); } // Process max 3 at a time await processWithLimit(urls, 3, url => fetch(url));
Common Mistakes
Forgetting await
javascript// Bug: Returns Promise, not data async function getData() { const data = fetch('/api/data').then(r => r.json()); return data; // Promise, not actual data! } // Fix async function getData() { const data = await fetch('/api/data').then(r => r.json()); return data; }
await in forEach
javascript// Bug: forEach doesn't wait for async async function processBad(items) { items.forEach(async item => { await processItem(item); // Not actually waiting! }); console.log('Done'); // Runs before processing completes } // Fix: Use for...of async function processGood(items) { for (const item of items) { await processItem(item); } console.log('Done'); } // Or Promise.all for parallel async function processParallel(items) { await Promise.all(items.map(item => processItem(item))); console.log('Done'); }
Unhandled Rejections
javascript// Bug: No error handling async function riskyCode() { const data = await fetch('/api/data'); // Could throw! return data.json(); } // Fix: Always handle errors async function safeCode() { try { const data = await fetch('/api/data'); return await data.json(); } catch (error) { console.error('Fetch failed:', error); return null; } }
Conclusion
Promises and async/await are essential JavaScript skills. Use Promises for their composability with Promise.all and other methods. Use async/await for readable, sequential-looking async code. Remember: async/await is just syntax sugar over Promises - understanding Promises is key to mastering async JavaScript.