Chapter 10: The Asynchronous Adventures
· 15 min read
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