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 Twitter or tag me here to discuss about this article.
Top comments (2)
for await...of ?
Is doing the job performance wise, but has no practical uses for this specific use case. We would have to loop once to build the array of Promises, then loop another time within the for await...of in order to get the datas.
Another downside is that you can't (I think ?) handle the Promises rejection nicely within a for await...of. If one of them reject, you'll have a runtime errors, where Promise.all() has a fail fast built-in, and will reject if any of the Promise reject