14. Asynchronous Programming

Synchronous vs Asynchronous

Synchronous Code

console.log("Start");
console.log("Middle");
console.log("End");
// Output: Start, Middle, End

Asynchronous Code

console.log("Start");

setTimeout(() => {
    console.log("Async operation completed");
}, 1000);

console.log("End");
// Output: Start, End, Async operation completed

Callbacks

Basic Callback

function fetchData(callback) {
    setTimeout(() => {
        const data = { user: "John", age: 30 };
        callback(data);
    }, 1000);
}

fetchData((data) => {
    console.log("Data received:", data);
});

Callback Hell

getUser(123, (user) => {
    getPosts(user.id, (posts) => {
        getComments(posts[0].id, (comments) => {
            console.log("Comments:", comments);
        }, (error) => {
            console.error("Error getting comments:", error);
        });
    }, (error) => {
        console.error("Error getting posts:", error);
    });
}, (error) => {
    console.error("Error getting user:", error);
});

Promises

Creating Promises

function fetchData() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const success = Math.random() > 0.5;
            if (success) {
                resolve({ user: "John", age: 30 });
            } else {
                reject(new Error("Failed to fetch data"));
            }
        }, 1000);
    });
}

// Using the promise
fetchData()
    .then(data => {
        console.log("Success:", data);
    })
    .catch(error => {
        console.log("Error:", error.message);
    });

Promise Methods

// Promise.resolve()
const resolvedPromise = Promise.resolve("Success");
resolvedPromise.then(result => console.log(result)); // "Success"

// Promise.reject()
const rejectedPromise = Promise.reject(new Error("Failed"));
rejectedPromise.catch(error => console.log(error.message)); // "Failed"

// Promise.all() - All must resolve
const promise1 = Promise.resolve(1);
const promise2 = Promise.resolve(2);
const promise3 = Promise.resolve(3);

Promise.all([promise1, promise2, promise3])
    .then(results => console.log(results)) // [1, 2, 3]
    .catch(error => console.log("One failed:", error));

// Promise.race() - First to settle wins
const fast = new Promise(resolve => setTimeout(() => resolve("fast"), 100));
const slow = new Promise(resolve => setTimeout(() => resolve("slow"), 1000));

Promise.race([fast, slow])
    .then(result => console.log(result)); // "fast"

// Promise.allSettled() (ES11+) - Wait for all to settle
const promises = [
    Promise.resolve("success"),
    Promise.reject("error"),
    Promise.resolve("another success")
];

Promise.allSettled(promises)
    .then(results => {
        results.forEach(result => {
            if (result.status === "fulfilled") {
                console.log("Fulfilled:", result.value);
            } else {
                console.log("Rejected:", result.reason);
            }
        });
    });

// Promise.any() (ES12+) - First to resolve wins
const promises2 = [
    Promise.reject("error1"),
    Promise.resolve("success"),
    Promise.reject("error2")
];

Promise.any(promises2)
    .then(result => console.log("First success:", result)) // "success"
    .catch(errors => console.log("All failed:", errors));

ES2024/ES2025 Updates

// Promise.withResolvers() (ES2024)
const { promise, resolve, reject } = Promise.withResolvers();
setTimeout(() => resolve('done'), 10);
promise.then(console.log); // 'done'

// Promise.try() (ES2025)
function mightThrow() { if (Math.random() < 0.5) throw new Error('nope'); return 42; }
Promise.try(mightThrow)
    .then(value => console.log('value:', value))
    .catch(err => console.error('caught:', err.message));

Async/Await (ES8+)

Basic Async Function

async function fetchUserData() {
    try {
        const response = await fetch('/api/user');
        const userData = await response.json();
        return userData;
    } catch (error) {
        console.error("Error fetching user data:", error);
        throw error;
    }
}

// Usage
async function main() {
    try {
        const user = await fetchUserData();
        console.log("User data:", user);
    } catch (error) {
        console.log("Failed to get user data");
    }
}

main();

Parallel Execution

async function fetchMultipleData() {
    try {
        // Sequential (slow)
        const user = await fetch('/api/user');
        const posts = await fetch('/api/posts');

        // Parallel (fast)
        const [userResponse, postsResponse] = await Promise.all([
            fetch('/api/user'),
            fetch('/api/posts')
        ]);

        const userData = await userResponse.json();
        const postsData = await postsResponse.json();

        return { user: userData, posts: postsData };
    } catch (error) {
        console.error("Error:", error);
    }
}

Error Handling with Async/Await

async function processData() {
    try {
        const data = await riskyOperation();
        const processed = await processData(data);
        await saveData(processed);
        console.log("All operations completed successfully");
    } catch (error) {
        console.error("An error occurred:", error.message);
        // Handle error appropriately
        await logError(error);
    } finally {
        // Cleanup code
        await cleanup();
    }
}

Generators (ES6+)

Basic Generator

function* numberGenerator() {
    yield 1;
    yield 2;
    yield 3;
}

const generator = numberGenerator();

console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 2, done: false }
console.log(generator.next()); // { value: 3, done: false }
console.log(generator.next()); // { value: undefined, done: true }

Async Generators (ES9+)

async function* asyncGenerator() {
    yield await Promise.resolve(1);
    yield await Promise.resolve(2);
    yield await Promise.resolve(3);
}

async function consumeAsyncGenerator() {
    for await (const value of asyncGenerator()) {
        console.log(value);
    }
}

consumeAsyncGenerator(); // 1, 2, 3

Event Loop

Understanding the Event Loop

console.log("Start");

setTimeout(() => {
    console.log("Timeout 1");
}, 0);

Promise.resolve().then(() => {
    console.log("Promise 1");
});

setTimeout(() => {
    console.log("Timeout 2");
}, 0);

Promise.resolve().then(() => {
    console.log("Promise 2");
});

console.log("End");

// Output order:
// Start
// End
// Promise 1
// Promise 2
// Timeout 1
// Timeout 2

Real-World Example: API Calls

class ApiClient {
    constructor(baseUrl) {
        this.baseUrl = baseUrl;
    }

    async get(endpoint) {
        try {
            const response = await fetch(`${this.baseUrl}${endpoint}`);
            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }
            return await response.json();
        } catch (error) {
            console.error(`GET ${endpoint} failed:`, error);
            throw error;
        }
    }

    async post(endpoint, data) {
        try {
            const response = await fetch(`${this.baseUrl}${endpoint}`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify(data),
            });
            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }
            return await response.json();
        } catch (error) {
            console.error(`POST ${endpoint} failed:`, error);
            throw error;
        }
    }
}

// Usage
const api = new ApiClient('https://jsonplaceholder.typicode.com');

async function fetchUserAndPosts(userId) {
    try {
        const [user, posts] = await Promise.all([
            api.get(`/users/${userId}`),
            api.get(`/posts?userId=${userId}`)
        ]);

        console.log('User:', user);
        console.log('Posts:', posts);
    } catch (error) {
        console.error('Failed to fetch data:', error);
    }
}

fetchUserAndPosts(1);

Best Practices

1. Use Async/Await for Cleaner Code

// Instead of this:
function getUser() {
    return fetch('/api/user')
        .then(response => response.json())
        .then(user => {
            return fetch(`/api/posts?userId=${user.id}`)
                .then(response => response.json());
        });
}

// Do this:
async function getUser() {
    const userResponse = await fetch('/api/user');
    const user = await userResponse.json();
    const postsResponse = await fetch(`/api/posts?userId=${user.id}`);
    const posts = await postsResponse.json();
    return { user, posts };
}

2. Handle Errors Properly

async function robustApiCall() {
    try {
        const response = await fetch('/api/data');
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }
        const data = await response.json();
        return data;
    } catch (error) {
        if (error.name === 'TypeError') {
            // Network error
            throw new Error('Network connection failed');
        }
        throw error; // Re-throw other errors
    }
}

3. Avoid Mixing Callbacks and Promises

// Bad: mixing styles
function oldStyleFunction(callback) {
    setTimeout(() => callback(null, "result"), 1000);
}

oldStyleFunction((error, result) => {
    if (error) return console.error(error);
    console.log(result);
});

// Good: convert to promises
function promisifiedFunction() {
    return new Promise((resolve, reject) => {
        oldStyleFunction((error, result) => {
            if (error) reject(error);
            else resolve(result);
        });
    });
}

promisifiedFunction().then(result => console.log(result));

Next Steps

Asynchronous programming is essential for modern JavaScript applications. Next, let's explore modules, which help organize and share code.