Chapter 10: The Asynchronous Adventures
JavaScript Land operates on a single thread, but it has clever ways to handle time-consuming tasks without blocking the main road of execution.
The Event Loop - The Great Coordinator
console.log("1. First"); setTimeout(() => { console.log("4. Timeout"); // Macrotask - goes to macrotask queue }, 0); Promise.resolve().then(() => { console.log("3. Promise"); // Microtask - goes to microtask queue }); console.log("2. Second"); // Output: 1. First → 2. Second → 3. Promise → 4. Timeout // Microtasks (promises) have higher priority than macrotasks (setTimeout)
Callbacks - The Original Async Pattern
// Simple callback function fetchUserData(userId, callback) { setTimeout(() => { const userData = { id: userId, name: "Alice", email: "alice@example.com" }; callback(null, userData); // First param is error, second is data }, 1000); } fetchUserData(123, (error, user) => { if (error) { console.error("Error:", error); } else { console.log("User:", user); } }); // Callback Hell - the pyramid of doom fetchUserData(123, (error, user) => { if (error) { console.error(error); } else { fetchUserPosts(user.id, (error, posts) => { if (error) { console.error(error); } else { fetchPostComments(posts[0].id, (error, comments) => { if (error) { console.error(error); } else { console.log("Comments:", comments); // This nesting can go on forever... 😱 } }); } }); } });
Promises - The Hope Bringers
// Creating a promise function fetchData(url) { return new Promise((resolve, reject) => { setTimeout(() => { if (url.includes("valid")) { resolve({ data: "Here's your data!", url }); } else { reject(new Error("Invalid URL")); } }, 1000); }); } // Using promises fetchData("https://valid-api.com") .then(result => { console.log("Success:", result); return fetchData("https://another-valid-api.com"); // Chain another promise }) .then(result => { console.log("Second success:", result); }) .catch(error => { console.error("Error:", error.message); }) .finally(() => { console.log("Cleanup actions here"); }); // Promise methods for handling multiple promises const promise1 = fetchData("https://valid-api1.com"); const promise2 = fetchData("https://valid-api2.com"); const promise3 = fetchData("https://valid-api3.com"); // Wait for all to complete (fails if any fail) Promise.all([promise1, promise2, promise3]) .then(results => { console.log("All succeeded:", results); }) .catch(error => { console.error("At least one failed:", error); }); // Wait for first to complete Promise.race([promise1, promise2, promise3]) .then(result => { console.log("First to finish:", result); }); // Wait for all to settle (succeed or fail) Promise.allSettled([promise1, promise2, promise3]) .then(results => { results.forEach((result, index) => { if (result.status === 'fulfilled') { console.log(`Promise ${index + 1} succeeded:`, result.value); } else { console.log(`Promise ${index + 1} failed:`, result.reason); } }); });
Async/Await - The Syntactic Sugar Heroes
// Converting promise chains to async/await async function fetchUserProfile(userId) { try { console.log("Fetching user data..."); const user = await fetchUserData(userId); console.log("Fetching user posts..."); const posts = await fetchUserPosts(user.id); console.log("Fetching post comments..."); const comments = await fetchPostComments(posts[0].id); return { user, posts, comments }; } catch (error) { console.error("Something went wrong:", error); throw error; // Re-throw if you want calling code to handle it } } // Using async function fetchUserProfile(123) .then(profile => { console.log("Complete profile:", profile); }) .catch(error => { console.error("Profile fetch failed:", error); }); // Async/await with Promise.all for parallel execution async function fetchMultipleUsers(userIds) { try { // These run in parallel, not sequentially const userPromises = userIds.map(id => fetchUserData(id)); const users = await Promise.all(userPromises); console.log("All users:", users); return users; } catch (error) { console.error("Failed to fetch users:", error); throw error; } } // Sequential vs Parallel execution async function sequentialFetch() { console.time("Sequential"); const user1 = await fetchUserData(1); // Wait 1 second const user2 = await fetchUserData(2); // Wait another 1 second const user3 = await fetchUserData(3); // Wait another 1 second console.timeEnd("Sequential"); // ~3 seconds total return [user1, user2, user3]; } async function parallelFetch() { console.time("Parallel"); const [user1, user2, user3] = await Promise.all([ fetchUserData(1), // All start at the same time fetchUserData(2), fetchUserData(3) ]); console.timeEnd("Parallel"); // ~1 second total return [user1, user2, user3]; }
Advanced Async Patterns
Async Iterators and Generators
// Async generator function async function* asyncNumberGenerator() { for (let i = 1; i <= 5; i++) { await new Promise(resolve => setTimeout(resolve, 100)); yield i; } } // Using async iterator async function consumeAsyncNumbers() { for await (const num of asyncNumberGenerator()) { console.log(`Received: ${num}`); } } // Creating async iterable objects const asyncIterable = { async *[Symbol.asyncIterator]() { yield await Promise.resolve(1); yield await Promise.resolve(2); yield await Promise.resolve(3); } };
Promise Pooling and Concurrency Control
// Limit concurrent operations async function promisePool(tasks, poolLimit) { const results = []; const executing = []; for (const task of tasks) { const promise = task().then(result => { executing.splice(executing.indexOf(promise), 1); return result; }); results.push(promise); if (tasks.length >= poolLimit) { executing.push(promise); if (executing.length >= poolLimit) { await Promise.race(executing); } } } return Promise.all(results); } // Usage example const tasks = Array.from({ length: 10 }, (_, i) => () => new Promise(resolve => setTimeout(() => resolve(`Task ${i + 1} completed`), Math.random() * 1000) ) ); promisePool(tasks, 3).then(results => { console.log("All tasks completed:", results); });
Error Handling Strategies
// Retry mechanism with exponential backoff async function fetchWithRetry(url, maxRetries = 3, delay = 1000) { for (let i = 0; i < maxRetries; i++) { try { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return await response.json(); } catch (error) { console.log(`Attempt ${i + 1} failed:`, error.message); if (i === maxRetries - 1) { throw error; // Last attempt, propagate error } // Exponential backoff const waitTime = delay * Math.pow(2, i); console.log(`Waiting ${waitTime}ms before retry...`); await new Promise(resolve => setTimeout(resolve, waitTime)); } } } // Timeout wrapper for promises function withTimeout(promise, timeoutMs) { return Promise.race([ promise, new Promise((_, reject) => setTimeout(() => reject(new Error('Operation timed out')), timeoutMs) ) ]); } // Usage withTimeout(fetchData('https://slow-api.com'), 5000) .then(result => console.log('Success:', result)) .catch(error => console.error('Failed:', error.message));
Real-World Async Patterns
Debouncing and Throttling Async Operations
// Debounce async function calls function debounceAsync(asyncFn, delay) { let timeoutId; let pending; return function(...args) { clearTimeout(timeoutId); if (pending) { pending.cancelled = true; } return new Promise((resolve, reject) => { pending = { cancelled: false }; const currentPending = pending; timeoutId = setTimeout(async () => { if (!currentPending.cancelled) { try { const result = await asyncFn.apply(this, args); resolve(result); } catch (error) { reject(error); } } }, delay); }); }; } // Example: Debounced search const searchAPI = async (query) => { const response = await fetch(`/api/search?q=${query}`); return response.json(); }; const debouncedSearch = debounceAsync(searchAPI, 300); // Input handler input.addEventListener('input', async (e) => { try { const results = await debouncedSearch(e.target.value); displayResults(results); } catch (error) { console.error('Search failed:', error); } });
Async Queue Implementation
class AsyncQueue { constructor(concurrency = 1) { this.concurrency = concurrency; this.running = 0; this.queue = []; } async add(task) { return new Promise((resolve, reject) => { this.queue.push({ task, resolve, reject }); this.process(); }); } async process() { if (this.running >= this.concurrency || this.queue.length === 0) { return; } this.running++; const { task, resolve, reject } = this.queue.shift(); try { const result = await task(); resolve(result); } catch (error) { reject(error); } finally { this.running--; this.process(); // Process next item } } } // Usage const queue = new AsyncQueue(2); // Max 2 concurrent tasks for (let i = 0; i < 10; i++) { queue.add(async () => { console.log(`Starting task ${i}`); await new Promise(resolve => setTimeout(resolve, 1000)); console.log(`Completed task ${i}`); return `Result ${i}`; }); }
Common Async Pitfalls
// Pitfall 1: Forgetting to await async function pitfall1() { const promise = fetchData(); // Forgot await! console.log(promise); // Logs Promise object, not data } // Pitfall 2: Awaiting in loops incorrectly async function pitfall2(urls) { // ❌ Sequential - slow const results = []; for (const url of urls) { results.push(await fetch(url)); // Waits for each one } // ✅ Parallel - fast const promises = urls.map(url => fetch(url)); const results2 = await Promise.all(promises); } // Pitfall 3: Not handling rejected promises async function pitfall3() { // ❌ Unhandled rejection const data = await riskyOperation(); // If this throws... // ✅ Proper handling try { const data = await riskyOperation(); } catch (error) { // Handle error } } // Pitfall 4: Creating accidental race conditions let cache = null; async function pitfall4() { if (!cache) { cache = await fetchExpensiveData(); // Multiple calls create race } return cache; }
Next Chapter: Chapter 11: The Prototype and Inheritance Saga
Previous Chapter: Chapter 9: The Hoisting Phenomenon
Table of Contents: JavaScript Guide
