Back to Articles

Chapter 10: The Asynchronous Adventures

July 27, 202515 min read
javascriptasyncpromisesasync-awaitevent-loopcallbacksasynchronousmicrotasks
Chapter 10: The Asynchronous Adventures

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