JavaScript  

Chapter 13: Advanced Asynchronous JavaScript: async and await

In Chapter 11, we learned about Promises for handling asynchronous code, often involving complex .then() chains. async and await keywords, introduced in ES2017, provide a much cleaner, more readable, and arguably safer way to write Promise-based code, making it look almost exactly like synchronous code.

1. The async Keyword

Any function declared with the async keyword automatically returns a Promise.

JavaScript

// An async function
async function getUserData() {
    // If the function returns a value, that value is wrapped in a resolved Promise.
    return 'User data is ready';
}

getUserData().then(data => console.log(data)); // Output: User data is ready

2. The await Keyword

The await keyword can only be used inside an async function. It pauses the execution of the async function until the Promise it precedes is settled (either resolved or rejected).

The value of the await expression is the resolved value of the Promise.

Rewriting fetch with async/await

Compare this to the .then() chain from Chapter 11. The code now reads naturally from top to bottom.

JavaScript

async function fetchAndDisplayUser() {
    const userId = 1;
    const url = `https://jsonplaceholder.typicode.com/users/${userId}`;

    try {
        // 1. Await the network response (Response object is resolved)
        const response = await fetch(url);

        // Check for HTTP errors
        if (!response.ok) {
            throw new Error(`Failed to fetch user: ${response.status}`);
        }

        // 2. Await the JSON parsing (Actual JavaScript object is resolved)
        const user = await response.json();

        // 3. Process the result (synchronous-looking code)
        console.log(`User Name: ${user.name}`);
        console.log(`User Email: ${user.email}`);

        return user;
    } catch (error) {
        // Catches errors from fetch() or the manual throw new Error()
        console.error('Error in fetchAndDisplayUser:', error.message);
    }
}

fetchAndDisplayUser();

3. Error Handling with try...catch

One of the biggest advantages of async/await is the ability to use the standard try...catch block to handle Promise rejections. Any error thrown (or any rejected Promise that is awaited) will be caught by the catch block, just like synchronous errors.

4. Concurrent Operations (Promise.all)

Sometimes, you need to execute multiple asynchronous operations at the same time and wait for all of them to finish. Using await sequentially is too slow if the operations are independent.

Promise.all(iterable) takes an array of promises and returns a single Promise that resolves when all of the promises in the iterable have resolved, or rejects immediately if any of them reject.

JavaScript

async function fetchAllData() {
    const todosPromise = fetch('https://jsonplaceholder.typicode.com/todos/1').then(res => res.json());
    const postsPromise = fetch('https://jsonplaceholder.typicode.com/posts/1').then(res => res.json());

    // Wait for BOTH promises to resolve simultaneously
    const [todo, post] = await Promise.all([todosPromise, postsPromise]);

    console.log('Todo data:', todo);
    console.log('Post data:', post);
}

fetchAllData(); // Much faster than awaiting each one sequentially

By using async/await, you write cleaner, more maintainable asynchronous code, making complex data fetching and coordination tasks much easier to manage.