4. Async & Await

Follow along with code examples here!

Intro: Promise Returning Is Tricky

What's wrong with this code? Why does it print undefined?

fetch('https://pokeapi.co/api/v2/pokemon/pikachu')
  .then((response) => {
    if (!response.ok) {
      throw Error(`Fetch failed. ${response.status} ${response.statusText}`);
    }
    const readingPromise = response.json();
  })
  .then((data) => {
    console.log(data); // prints undefined
  })
  .catch((error) => {
    console.log("Error caught!");
    console.log(error.message);
  })
Answer

We forgot to return readingPromise from the first .then callback, so the second .then callback never got a value!

The benefits of async/await

Using the async/await syntax with try and catch has a number of benefits. The main ones being readability and debuggability.

  • We can write async code in a synchronous-like manner

  • We avoid having to write a bunch of callbacks

  • We can avoid common mistakes made when using callbacks

  • try/catch is a more general-purpose way of handling errors that can be used for more than just fetching.

Fetching "Synchronously" with Async/Await

When fetching, it is important to observe that we have two sequential asynchronous processes involved:

  1. fetching

  2. reading the response body's ReadableStream.

And we've handled those processes using Promises like this:

  • fetch() is an asynchronous function so it returns a promise.

  • The first .then callback handles that promise's response value.

  • In that callback we start reading in the response data with response.json(), which returns another promise.

  • The second .then callback handles that promise's responseData value.

Effectively, we are saying, "wait for the fetch() promise to resolve, then wait for the response.json() promise to resolve, then begin working with the data".

However, working with callbacks can be conceptually difficult to follow at times. So, an alternate syntax was created to achieve the same effect but without the need for callbacks.

This approach utilizes the async and await keywords:

  • The await keyword is placed before any function call that returns a Promise

    • It causes our code to pause and wait for the Promise to resolve.

    • It then "unpacks" the Promise and returns the resolved value.

  • The async keyword is placed before any function that uses await inside:

    • Any value returned by this function now will be wrapped in a Promise

Handling Errors with Try/Catch

There are some functions (like fetch() and response.json()) that are capable of throwing an error.

We can manually throw our own errors too using throw new Error('message'). When an error is thrown, the program crashes immediately:

try/catch is the standard way to handle those errors and it will prevent the application from crashing. Instead, we can handle any errors that occur and continue on:

So, when using fetch() or response.json(), we should always use try and catch, allowing us to gracefully handle errors:

Last updated