Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 6

Simplifying Asynchronous JavaScript with Async/Await

Recommended resources

Hello! Welcome to the final lesson in our "JavaScript Essentials for React" module.

In the last lesson, you mastered how to handle asynchronous operations using Promises with .then(), .catch(), and .finally(). You saw how chaining promises creates a much cleaner workflow than the old "Callback Hell." Today, we'll build directly on that knowledge by learning a more modern and even cleaner syntax.

This lesson covers your final learning outcome for this module: to use async/await syntax to write cleaner asynchronous code. async/await is often described as "syntactic sugar" on top of Promises. It doesn't introduce any new functionality you couldn't achieve with promise chains, but it allows you to write asynchronous code that looks and reads almost exactly like synchronous code, making it significantly easier to write and debug.

1. From Promise Chains to Synchronous-Style Code

Let's quickly recap the promise chain structure. To perform sequential asynchronous tasks, you chain .then() methods, passing the result of one operation to the next.

// The .then() chain style from our last lesson
fetch('some-url')
  .then(response => response.json())
  .then(data => {
    // do something with data
  })
  .catch(error => {
    // handle any errors in the chain
  });

This is clear, but the nested functions and chaining syntax can still feel a bit different from regular, top-to-bottom code. async/await was introduced in ES2017 to solve this.

Let's watch a video that perfectly demonstrates how to convert a standard promise chain into the async/await syntax.

JavaScript Async Await

This video from Web Dev Simplified provides a direct comparison between code using .then() and the same logic rewritten with async/await. It's the perfect bridge from our previous lesson.

Watch the following two segments: Introduction (00:00 - 01:03): This sets up the initial promise-based example that will be refactored. Introducing async and await (03:03 - 05:19): Pay close attention to how the .then() chain is transformed into what looks like simple, sequential variable assignments using the async and await keywords.

As you saw, the core idea is simple but powerful:

  • The async keyword is used to declare a function as asynchronous.
  • The await keyword is used to pause the function's execution until a Promise settles.

2. The Rules of async/await

This new syntax comes with two fundamental rules that are crucial to understand. For a more detailed breakdown, let's turn to a comprehensive text resource.

Async/await - The Modern JavaScript Tutorial

The article 'Async/await' from The Modern JavaScript Tutorial is an excellent, in-depth guide. We'll focus on the sections that define the core mechanics.

Please read the first two sections: 'Async functions': This section explains Rule #1: An async function always returns a promise. 'Await': This section explains Rule #2: The await keyword only works inside an async function. It also shows a great example of refactoring a .then chain into async/await, similar to the video.

Let's summarize those key takeaways:

  1. async Functions Always Return a Promise: If you return a value from an async function (like return 10;), JavaScript automatically wraps it in a resolved Promise for you. This is why you can call .then() on the result of an async function.
  2. await Pauses the async Function: When the JavaScript engine encounters await, it suspends the execution of that async function, allows other parts of your program to run, and resumes the function only when the awaited promise is settled. If fulfilled, it unwraps the value; if rejected, it throws an error.

3. Error Handling with try...catch

So, if await throws an error when a promise is rejected, how do we handle it? We don't use .catch() anymore. Instead, we use a standard JavaScript construct you might already be familiar with: the try...catch block.

This is one of the biggest advantages of async/await—error handling looks just like it does in synchronous code.

Let's see how this works.

JavaScript Async Await

We'll return to the Web Dev Simplified video to see how try...catch is integrated.

Watch the segment on error handling from 05:19 to 06:24. Notice how the code that can fail is placed inside the try block, and the error-handling logic goes into the catch block.

By wrapping your asynchronous calls in a try block, you can catch any rejected promise from any await statement within that block. The error is then available in the catch(error) block, just as if you were catching a synchronous throw new Error().


4. A Practical Refactor

Let's apply everything we've learned and refactor the fetch example from our last lesson.

Before: The Promise Chain

fetch('https://jsonplaceholder.typicode.com/users/1')
  .then(response => {
    if (!response.ok) {
      throw new Error(`HTTP error! Status: ${response.status}`);
    }
    return response.json();
  })
  .then(userData => {
    console.log(`Successfully fetched user: ${userData.name}`);
    return fetch(`https://jsonplaceholder.typicode.com/posts?userId=${userData.id}`);
  })
  .then(response => {
    if (!response.ok) {
      throw new Error(`HTTP error fetching posts! Status: ${response.status}`);
    }
    return response.json();
  })
  .then(posts => {
    console.log(`Found ${posts.length} posts. The first post title is: "${posts[0].title}"`);
  })
  .catch(error => {
    console.error("An error occurred:", error.message);
  });

After: Using async/await
Now, let's rewrite this logic inside an async function. Notice how flat and readable it becomes.

JavaScript Async/Await Example with Fetch API
This image shows a clean example of an async function named `getData` that uses `await` to fetch and parse data from an API, demonstrating the readability of the syntax.

Here is our full refactored code. I recommend you run this in your browser's console to see it work.

// We wrap our logic in an async function to use await.
async function fetchUserDataAndPosts() {
  try {
    console.log("Starting the fetch process...");

    // 1. Fetch the user
    const userResponse = await fetch('https://jsonplaceholder.typicode.com/users/1');
    if (!userResponse.ok) {
      throw new Error(`HTTP error fetching user! Status: ${userResponse.status}`);
    }
    const userData = await userResponse.json();
    console.log(`Successfully fetched user: ${userData.name}`);

    // 2. Fetch the posts for that user
    const postsResponse = await fetch(`https://jsonplaceholder.typicode.com/posts?userId=${userData.id}`);
    if (!postsResponse.ok) {
      throw new Error(`HTTP error fetching posts! Status: ${postsResponse.status}`);
    }
    const posts = await postsResponse.json();
    console.log(`Found ${posts.length} posts. The first post title is: "${posts[0].title}"`);

  } catch (error) {
    // A single catch block handles any error from any `await` or `throw` above.
    console.error("An error occurred:", error.message);
  } finally {
    // Just like with promises, a `finally` block in a try...catch
    // will execute regardless of success or failure.
    console.log("Fetch operation has completed.");
  }
}

// To run the code, we simply call our async function.
fetchUserDataAndPosts();

The logic is identical, but the async/await version is arguably much easier to follow for most developers. It reads like a simple list of instructions, which is a huge win for code maintainability.


Conclusion

Congratulations on completing the "JavaScript Essentials for React" module! With async/await, you now have the most modern and powerful tool for handling asynchronous operations in JavaScript.

Key Takeaways:

  • async/await is syntactic sugar over Promises, providing a cleaner, more readable syntax.
  • The async keyword declares that a function will handle asynchronous operations and will always return a Promise.
  • The await keyword can only be used inside an async function. It pauses execution and waits for a Promise to settle before proceeding.
  • Error handling is done with standard try...catch blocks, which makes handling rejected promises feel more like handling synchronous errors.
  • The result is asynchronous code that is easier to read, write, and debug because it looks synchronous.

Next Up:

You've built a fantastic foundation. You're proficient with arrow functions, destructuring, array methods, and now, the full picture of asynchronous JavaScript from Promises to async/await. You are officially ready to start building with React!

In our next lesson, we will kick off the "React Fundamentals" module by exploring React's core philosophy: component-based architecture. You'll learn what components are and why they are such a powerful paradigm for building user interfaces.