Stephen Sun

Software engineer based in Houston, Texas.

I'm a detail-oriented individual who thrives in fast-paced team environments. I have experience across different industries, working with both front end and back end technologies.

How to use async await in loops

As developers, we frequently find ourselves needing to perform certain asynchronous tasks.

What if we want to do this in a loop?

Let's dive into a few examples of common loops in JavaScript and see how async/await behaves in each scenario.

We'll be using the public Dog API for all of our examples: Dog API

ForEach Loop

Let's start off with an example of a forEach loop.

const dogUrls = [
  'https://dog.ceo/api/breeds/image/random',
  'https://dog.ceo/api/breed/hound/images/random',
  'https://dog.ceo/api/breed/hound/afghan/images/random',
]

const getDogImage = async url => {
  const res = await fetch(url)
  const data = await res.json()
  document.write(`<img src=${data.message} />`)
  console.log('displaying dog image')
}

const fetchDogs = () => {
  dogUrls.forEach(async url => await getDogImage(url))
  console.log('Done!')
}

fetchDogs()

Let's break down the different parts of our example:

  1. dogUrls is an array of urls from the Dog Api (https://dog.ceo/dog-api/documentation/)
  2. getDogImage() is our asynchronous function that takes in a url, fetches our dog image, and displays it on our webpage
  3. Once the image is displayed, we'll print "displaying dog image" to the console to indicate the end of an iteration
  4. fetchDogs() is our main function that uses a forEach loop that calls our asynchronous getDogImage() function
  5. Once the forEach loop completes, we'll print "Done!" to the console to indicate the end of all tasks

What we expect to happen is the following order:

  1. For each iteration, we should take a url from the array and fetch the image from the Dog API
  2. Display the dog image in the webpage
  3. Print "displaying dog image" to the console
  4. Iterate three times
  5. print "Done!" to the console

But what we see is that the "Done!" message is being printed to the console before all the tasks in our forEach loop.

This is because forEach loops are synchronous by design, and won't wait for promises to resolve/complete.

As a result, it is not possible to perform asynchronous tasks inside forEach loops, and we should only use this loop when dealing with synchronous tasks.

For-Of Loop

Now, let's take our previous example and use a for-of loop instead.

const dogUrls = [
  'https://dog.ceo/api/breeds/image/random',
  'https://dog.ceo/api/breed/hound/images/random',
  'https://dog.ceo/api/breed/hound/afghan/images/random',
]

const getDogImage = async url => {
  const res = await fetch(url)
  const data = await res.json()
  document.write(`<img src=${data.message} />`)
  console.log('displaying dog image')
}

const fetchDogs = async () => {
  for (const url of dogUrls) {
    await getDogImage(url)
  }
  console.log('Done!')
}

fetchDogs()

As we can see, all of the tasks execute in the order that we expected in the forEach example.

Whenever we await getDogImage(), the for loop waits for all of the tasks in getDogImage() to complete before moving on to the next iteration.

Also, the "Done" message is printed to the console only after all the iterations of the for loop completes.

As a result, we should use for-of loops when we need to perform asynchronous tasks in a specific order.

Map Method

What if we don't care about the order that our asynchronous tasks execute?

Also, what if we care about performance, and we want to fetch/display our dog images as fast as possible?

const dogUrls = [
  'https://dog.ceo/api/breeds/image/random',
  'https://dog.ceo/api/breed/hound/images/random',
  'https://dog.ceo/api/breed/hound/afghan/images/random',
]

const getDogImage = async url => {
  const res = await fetch(url)
  const data = await res.json()
  document.write(`<img src=${data.message} />`)
  console.log('displaying dog image')
}

const fetchDogs = async () => {
  const promises = dogUrls.map(async url => await getDogImage(url))
  await Promise.all(promises)
  console.log('Done!')
}

fetchDogs()

Instead of waiting for each iteration to complete before the next, we can create an array of promises and fire all of them off at the same time.

The map method always returns an array, and in our case, an array of promises that call our getDogImage() function.

We then pass our array of promises to Promise.all, which will execute all of the promises at once.

Again, if your asynchronous tasks need to be executed in a specific order, then a for-of loop should be used.

Reduce Method

What if we want both order and speed?

const dogUrls = [
  'https://dog.ceo/api/breeds/image/random',
  'https://dog.ceo/api/breed/hound/images/random',
  'https://dog.ceo/api/breed/hound/afghan/images/random',
]

const getDogImage = async url => {
  const res = await fetch(url)
  const data = await res.json()
  document.write(`<img src=${data.message} />`)
  console.log('displaying dog image')
}

const fetchDogs = async () => {
  await dogUrls.reduce(async (promise, url) => {
    await getDogImage(url)
    await promise
  }, Promise.resolve())
  console.log('Done!')
}

fetchDogs()

Reduce to the rescue!

The reduce method is commonly used when we want tasks to happen in a specific order, and each task depends on the result of the previous task.

In our example, we start our reduce method with Promise.resolve(), which is an already resolved Promise object.

We then await our getDogImage() function first, before we await our accumulator (promise).

This makes our reduce method performant, while executing our tasks sequentially.