The Pitfalls of Async/Await in Array Loops

The Pitfalls of Async/Await in Array Loops

Using async/await while looping through arrays in Javascript loop seems simple, but there’s some non-intuitive behavior to look out for when combining the two.

Let’s take a look at three different examples to see what you should look out for, and which loop is best for specific use cases.


forEach

If you only take one thing away from this article, let it be this: **async**/**await** doesn’t work in **Array.prototype.forEach**. Let’s dive into an example to see why:

const urls = [
  'https://jsonplaceholder.typicode.com/todos/1',
  'https://jsonplaceholder.typicode.com/todos/2',
  'https://jsonplaceholder.typicode.com/todos/3'
];

async function getTodos() {
  await urls.forEach(async (url, idx) => { 
    const todo = await fetch(url);
    console.log(`Received Todo ${idx+1}:`, todo);
  });
  
  console.log('Finished!');
}

getTodos();

Finished!
Received Todo 2, Response: { ··· }
Received Todo 1, Response: { ··· }
Received Todo 3, Response: { ··· }

⚠️ Problem 1:

The code above will happily execute. However, notice that Finished!was logged out first despite our use of await before urls.forEach. The first problem is that you can’t await the entire loop when using forEach.

⚠️ ️Problem 2:

In addition, despite the use of await within the loop, it didn’t wait for each request to finish before executing the next one. So, the requests were logged out of order. If the first request takes longer than the following requests, it could still finish last.

For both of those reasons, forEach should not be relied upon if you’re using async/await.


Promise.all

Let’s solve the issue of waiting on the entire loop to finish. Since the await operation creates a promise under the hood, we can use Promise.all to execute all of the requests in parallel, then await the final results from the loop:

const urls = [
  'https://jsonplaceholder.typicode.com/todos/1',
  'https://jsonplaceholder.typicode.com/todos/2',
  'https://jsonplaceholder.typicode.com/todos/3'
];

async function getTodos() {
  const promises = urls.map(async (url, idx) => 
    console.log(`Received Todo ${idx+1}:`, await fetch(url))
  );

  await Promise.all(promises);

  console.log('Finished!');
}

getTodos();

Received Todo 1, Response: { ··· }
Received Todo 2, Response: { ··· }
Received Todo 3, Response: { ··· }
Finished!

We’ve solved the issue of waiting on every request to finish before continuing onward. It also appears that we resolved the issue of the requests happening out of order, but that’s not exactly the case.

As mentioned earlier, Promise.all executes all of the promises given to it in parallel. It won’t wait for the first request to come back before executing the second, or third request. For most purposes this is fine, and it’s a very performant solution. But, if you truly need each request to happen in order, Promise.all won’t solve for that.


for…of

We now know that forEach doesn’t respect async/await at all, and Promise.all only works if the order of execution doesn’t matter. Let’s look at a solution that solves for both cases.

The for...of loop executes in the order one would expect — waiting on each previous await operation to complete before moving on to the next:

const urls = [
  'https://jsonplaceholder.typicode.com/todos/1',
  'https://jsonplaceholder.typicode.com/todos/2',
  'https://jsonplaceholder.typicode.com/todos/3'
];

async function getTodos() {
  for (const [idx, url] of urls.entries()) {
    const todo = await fetch(url);
    console.log(`Received Todo ${idx+1}:`, todo);
  }

  console.log('Finished!');
}

getTodos();

Received Todo 1, Response: { ··· }
Received Todo 2, Response: { ··· }
Received Todo 3, Response: { ··· }
Finished!

I particularly like how this method allows the code to remain linear — which is one of the key benefits of using async/await. I find it much easier to read than the alternatives.

If you don’t need access to the index, the code becomes even more concise:

for (const url of urls) { ··· }

One of the major downsides of using a for...of loop is that it performs poorly compared to the other looping options in Javascript. However, the performance argument is negligible when using it to await asynchronous calls, since the intention is to hold up the loop until each call resolves. I typically only use for...of if asynchronous order of execution matters.

Note: You can also use basic for loops to get all of the benefits of for...of, but I like the simplicity and readability that for...of brings to the table.

👏 If you found this article helpful and would like to see more, please let me know by leaving some claps! 🔗 Follow for more articles like this.


Learn More

Machine Learning in JavaScript with TensorFlow.js

ES5 to ESNext — here’s every feature added to JavaScript since 2015

5 Javascript (ES6+) features that you should be using in 2019

Vuejs 2 Authentication Tutorial

Vue Authentication And Route Handling Using Vue-router

How to Deep Clone an Array in JavaScript

Build a CMS with Laravel and Vue

The Complete JavaScript Course 2019: Build Real Projects!

JavaScript Bootcamp - Build Real World Applications

JavaScript: Understanding the Weird Parts

Vue JS 2 - The Complete Guide (incl. Vue Router & Vuex)

The Full JavaScript & ES6 Tutorial - (including ES7 & React)

The Modern JavaScript Bootcamp (2019)

Originally published by Tory Walker at https://medium.com/dailyjs