Skip to main content

Welcome to a Grasp lesson

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

Lesson 18

Simplifying Asynchronous Code with Async/Await

Recommended resources

Hello again! It's great to see you back.

In our last lesson, we dug into JavaScript Promises and learned how to manage asynchronous outcomes using .then() for successes and .catch() for failures. This pattern, especially with chaining, is a massive improvement over old-school callbacks.

Today, we're going to build directly on that knowledge. We'll explore a more modern and intuitive syntax for working with Promises: async/await. The goal of this lesson is to learn how to write cleaner, more readable asynchronous code that accomplishes the same thing as .then() chains, but often in a more straightforward way. You'll find this syntax makes asynchronous code look and behave much more like the synchronous code you might be used to from languages like Python.

From Promise Chains to async/await

Let's start by looking at a typical Promise chain, similar to what we saw in the last lesson. Imagine we have two functions that return promises: makeRequest and processRequest.

// A typical .then() chain
makeRequest('Google')
  .then(response => {
    console.log('Response Received');
    return processRequest(response); // This also returns a promise
  })
  .then(processedResponse => {
    console.log(processedResponse);
  })
  .catch(err => {
    console.error(err);
  });

This code works perfectly well, but the nesting and callbacks can start to get a bit cumbersome as logic gets more complex. This is where async/await comes in. It's often described as "syntactic sugar" over Promises, meaning it's a different way to write the same logic, but it doesn't change how things work underneath.

Let's watch a video that perfectly illustrates how to convert the code above into the async/await style.

JavaScript Async Await

The video 'JavaScript Async Await' from Web Dev Simplified provides a fantastic side-by-side comparison of promise chains and async/await. It clearly demonstrates the syntax and the benefits.

Please watch from the beginning up to 5:19. The first part (until 2:45) shows the .then() approach, which will be a good recap. The second part (from 2:45 onwards) shows the conversion to async/await. Focus on how the code becomes flatter and more sequential.


The Two Core Keywords: async and await

As you saw in the video, async/await revolves around two keywords. Let's break down their roles:

  1. async: This keyword is placed before a function declaration. It does two critical things:

    • It enables the use of the await keyword inside that function.
    • It automatically ensures that the function returns a Promise. If you explicitly return a value (e.g., return 42), the async function will wrap it in a fulfilled Promise for you.
  2. await: This keyword can only be used inside an async function. When you place await in front of an expression that returns a Promise (like fetch() or another async function), it does the following:

    • It "pauses" the execution of the async function until the Promise settles.
    • If the Promise fulfills, await returns the fulfilled value.
    • If the Promise rejects, it throws an error, which we'll need to handle.

Here is the code from the video, rewritten to look like this:

// The same logic with async/await
async function doWork() {
  const response = await makeRequest('Google');
  console.log('Response Received');
  const processedResponse = await processRequest(response);
  console.log(processedResponse);
}

Notice how the code reads from top to bottom, without any .then() callbacks. The await keyword effectively "unpacks" the value from the promise for you.


Handling Errors with try...catch

So, await gives us the successful value, but what about errors? In .then() chains, we use .catch(). With async/await, we use a construct you might already be familiar with from other programming languages: the try...catch block.

  • You wrap your "happy path" code—the code that might throw an error—inside the try { ... } block.
  • You write your error-handling logic inside the catch (error) { ... } block.

If any await-ed promise in the try block rejects, the execution immediately jumps to the catch block, where you can handle the error.

Let's continue with the same video to see exactly how this works.

JavaScript Async Await

This next segment shows how to add error handling to our async function using a try...catch block.

Watch from 5:19 to 6:24. Observe how the try block contains the core logic and how the catch block gracefully handles the rejected promise.

Here's our complete function with robust error handling:

async function doWork() {
  try {
    const response = await makeRequest('Facebook'); // This will fail
    console.log('Response Received');
    const processedResponse = await processRequest(response);
    console.log(processedResponse);
  } catch (err) {
    // If makeRequest() rejects, the code jumps here
    console.error(err);
  }
}

doWork(); // Don't forget to call the function!

This pattern is the standard for handling errors with async/await and is considered very readable because it clearly separates the success logic from the error-handling logic.


A Practical Example: async/await with fetch

Let's see this in the context of a real-world task: fetching data from an API. In the last lesson, we used fetch with .then() chains. Now, look how clean it becomes with async/await.

JavaScript async/await with Fetch API Example
This code defines an `async` function `getData` that uses `await` to call the `fetch` API. It then `await`s the `response.json()` call to parse the data before logging a result. This is a very common and readable pattern.

Let's break down the code in the image:

  1. We define getData as an async function.
  2. await fetch(...) pauses the function until the network request is complete and the Response object is available.
  3. await response.json() pauses the function again until the response body is fully parsed into a JavaScript object.
  4. Finally, we can work with the data as a regular synchronous variable.

For another excellent walkthrough of this entire concept, from the motivation to a real fetch example, the following video is a great resource.

Async Await vs. Promises - JavaScript Tutorial for beginners

The video 'Async Await vs. Promises' from ColorCode offers another great perspective, emphasizing the improved readability and providing a clear fetch example.

You can watch this video to reinforce the concepts. The most relevant parts are the motivation (0:00-2:38), the syntax introduction (2:38-7:08), the fetch example (7:08-10:53), and the try/catch explanation (14:13-18:40).

Test your understanding!

In the previous lesson, you worked with this function:

function createDelayedMessage(shouldSucceed) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (shouldSucceed) {
        resolve("Success: The message has arrived!");
      } else {
        reject(new Error("Failure: The message was lost."));
      }
    }, 1000);
  });
}

Your task is to write a new async function called handleMessage. This function should:

  1. Call createDelayedMessage(true).
  2. await the result.
  3. Log the successful result to the console.
  4. Wrap the logic in a try...catch block to handle potential errors, logging any error message to console.error().
  5. Finally, call your handleMessage() function to run it.

How would you write this function? What would happen if you changed the call to createDelayedMessage(false)?

Show answer

Here is the implementation:

async function handleMessage() {
  try {
    // Set shouldSucceed to true or false to test both paths
    const message = await createDelayedMessage(true); 
    console.log(message);
  } catch (error) {
    console.error(error.message);
  }
}

// Run the function
handleMessage();

If called with createDelayedMessage(true), the output after 1 second will be:
Success: The message has arrived!

If you change it to createDelayedMessage(false), the promise will reject, the catch block will execute, and the output after 1 second will be:
Failure: The message was lost.


Conclusion

You have now learned the modern, preferred way to handle asynchronous operations in JavaScript. While it's crucial to understand the Promises and .then() chains working underneath, async/await will be your primary tool for writing clean, maintainable, and less error-prone asynchronous code.

Key Takeaways:

  • async/await is syntactic sugar over Promises, making asynchronous code read like synchronous code.
  • The async keyword declares a function that returns a Promise and allows the use of await within it.
  • The await keyword pauses function execution until a Promise settles, and then returns its fulfilled value or throws an error if it rejects.
  • Error handling in async functions is done using standard try...catch blocks, which cleanly separates success and failure logic.

In our next lesson, we will dive deeper into the fetch() API. You've seen it used as an example here, but we will formally cover how to use it to get data from external REST APIs and handle different kinds of JSON responses—a cornerstone skill for any web developer.