Recently, I learned the hard way that await is not the solution to every promises.

At work, I had to write a piece of code that was looping for a lots of elements.

Bascially, it was looping over hundreds of elements, and was doing an HTTP request for each in order to retrieve some vitals informations.

It was something like that :

  //...
 const datas = [];

  for (const element of elements) {
    const result = await axios.get('https://pokeapi.co/api/v2/pokemon/ditto');
    datas.push(result);
  }
  // Use datas ...

This is a really simplified exemple using a free Pokemon API (We all have our favorite API's 🙈).
I didn't notice myself that it was causing a performance issue, it first came as an Eslint error :

    Unexpected `await` inside a loop.

🤔🤔🤔

It was time to dig and to follow the documentation link.

And just to make sure Eslint wasn't lying to me (You should trust him 100%), I did some testings ...

The test

Going back to our previous exemple, but with some console.time to evaluate the actual time it take for our loop.

const axios = require('axios');
const elements = new Array(45);

async function fetchPokemons() {
  const datas = [];

  console.time('Wrong way');
  for (const element of elements) {
    const result = await axios.get('https://pokeapi.co/api/v2/pokemon/ditto');
    datas.push(result);
  }

  console.timeEnd('Wrong way');

}

fetchPokemons();

Here is the Node code sample I used, feel free to try it out yourself.

It would be painful to make you guess how much time it took for our loop to finish, so here you go :

Between 12 and 13 seconds.

    Wrong way: 13.191s

Doesn't sound that bad for 45 HTTP calls, but let's see how it goes if we refactor as Eslint told us.

The refactor

async function fetchPokemons() {
  const promises = [];

  console.time('Nice way');
  
  for (const element of elements) {
    const result = axios.get('https://pokeapi.co/api/v2/pokemon/ditto');
    promises.push(result);
  }
  
  const results = await Promise.all(promises);
  const actualDatas = results.map((result) => result.data); // We need an extra loop to extract the data, and not having the requests stuff
  
  console.timeEnd('Nice way');
}

fetchPokemons();

So ... What happened ?

Well, we basically removed the await, and pushed all our unresolved promises into an array. Then we simply await for all of them to resolved, and we extract the datas.

Isn't it the same thing ?

Well .. Not really. Before we dive into the explanation, could you take a quick guess on how much time it take for us to gather all the datas ?

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

Between 0.8 and 1.5 seconds.

    Nice way: 1.448s

🤯

Did we just cut down the time by 10 ? Yes.

Explanation

It's pretty simple, previously we were waiting for each requests to resolve before launching the next one :

  • Launch first
  • Wait N seconds until it resolved
  • Launch seconds
  • Wait N seconds until it resolved
  • ...

Time add up a lot as we saw.

Now, it look like this :

  • Launch first
  • Launch second
  • ...
  • Wait for what's left to resolve

You got it ? By the time we were looping and launching everything, some - if not most, promises have already resolved !

Conclusion

Now you'll think twice before awaiting in a loop.

If you are a bit lost and don't really get what was happening here, I wrote an article that cover all the Promises basics for Javascript.

You can find the original article on the Othrys website and you can follow my Tweeter or tag me here to discuss about this article.