The introduction of the async and await keywords made it easier to handle asynchronous tasks in JavaScript. However, developers should be aware of some problematic aspects when it comes to using them inside loops.

What’s the problem? Basically, async and await work differently depending on which type of loop they are in.

Inside the native loops (i.e. for, for...of, for...in, while, do...while), the async keyword blocks the loop until the asynchronous task is over. For example:

async function printPokemonNames() {
    const pokemonIDs = [25, 120, 237];

    for (const pokemonID of pokemonIDs) {
        const pokemon = await fetch(
            `https://pokeapi.co/api/v2/pokemon/${pokemonID}`
        ).then(response => response.json());

        console.log(pokemon.name);
    }
}
printPokemonNames();

This code makes a query to the PokéAPI to print the name of three Pokémon. Each time it calls the asynchronous fetch method, the entire code pauses until the request is completed.

It works like this:

  1. Get the info for the first Pokémon using the API request
  2. Wait until the request is complete
  3. Print the name of the first Pokémon
  4. Get the info for the second Pokémon using the API request
  5. Wait until the request is complete
  6. Print the name of the second Pokémon
  7. Get the info for the third Pokémon using the API request
  8. Wait until the request is complete
  9. Print the name of the third Pokémon

On the other hand, when you loop over an array (or another iterable) using a method like forEach, map, reduce, etc. that requires a callback function. In those cases, the await keyword pauses the callback for that iteration but doesn’t stop the entire loop.

Let’s rewrite the previous example using forEach:


const pokemonIDs = [25, 120, 237];
pokemonIDs.forEach(async function(pokemonID) {
    const pokemon = await fetch(
        `https://pokeapi.co/api/v2/pokemon/${pokemonID}`
    ).then(response => response.json());

    console.log(pokemon.name);
});

Here, the call to fetch blocks the rest of the callback function (i.e. the call to console.log in this example), but it doesn’t prevent forEach from processing the second Pokémon even before completing the previous one. It works like this:

  1. Get the info for the first Pokémon using the API request
  2. Get the info for the second Pokémon using the API request
  3. Get the info for the third Pokémon using the API request
  4. Wait until the first request is complete, then print the name of the first Pokémon
  5. Wait until the second request is complete, then print the name of the second Pokémon
  6. Wait until the third request is complete, then print the name of the third Pokémon

An important aspect of this approach is that the three calls to fetch now happen practically at the same time. When you use a native loop, calls to fetch never happen simultaneously because the entire loop is blocked.

Which one is the best option? It depends! If you need to optimize speed, it might be a good idea to use a method.

However, if you have an array with hundreds or thousands of elements, and the asynchronous function is fetch, it means that you’ll make hundreds or thousands of HTTP requests at the same time! There’s no one-size-fits-all solution.