Understand JavaScript forEach() Behavior with async/await

JavaScript is often one of the first programming languages new programmers use not only due to its popularity and high demand but also because it offers flexibility to use a mixture of object-oriented programming and functional programming. Unfortunately, that flexibility can lead to problems that are not easy to identify such as the hidden problems of using Array.forEach() method to run asynchronous code.

Programmers use the forEach method to loop through each of the elements of an array to execute the same process. Unfortunately, the forEach method wasn’t meant to execute asynchronous callback functions, even though it is possible to use the keywords async and await with it.

Why is Array.forEach() not meant for asynchronous programming?

Looking for an answer explaining why the forEach doesn’t work as expected when using it with async and await keywords is rather complex.

If we verify the Mozilla MZN Web documentation and read through the Array.prototype.forEach() reference, you will find out there is a note saying forEach expects a synchronous function. While this site has become one of the main documentation references for JavaScript programmers, in theory, it is not the official documentation despite their excellent way to explain the concepts.

The official documentation for JavaScript is defined by Ecma International’s TC39, a group of JavaScript developers collaborating with the community to maintain and evolve the definition of JavaScript.

If we take a look at ECMA specifications defining the Array.forEach method, it doesn’t say anything about the method not designed for asynchronous operations.

If it is not in the documentation, why do you claim it is not meant for asynchronous programming?

This is when JavaScript shows one of those unexpected behaviors only developers who have noticed it, can share and advise to not use the forEach method with async and await because it doesn’t “await” or wait for a process to finish prior to continuing to the next process inside the callback function.

Why Array.forEach() with async/await does not actually wait?

Unless we have access to inspect the definition of the forEach method and understand what goes on behind, one way is to see an example of what happens when attempting to use forEach with async and await.

Let’s take a look at the following example and check a typical developer writing asynchronous logic inside the forEach callback without knowing the hidden problem he will face. For reference purposes, we are going to say this code is stored in the test.js file.

async function displayValuesWithWait(value) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("The current value is: ", value);
      resolve();
    }, 1000);
  });
}

async function valueLogger() {
  const values = [1, 2, 3, 4, 5];

  console.log("Starting to display values");

  values.forEach(async (value) => {

    console.log('About to run displayValuesWithWait() process for value ', value);

    await displayValuesWithWait(value);

    console.log('Finished displayValuesWithWait() for value ', value);
  });

  console.log("Finished displaying values");
}

valueLogger();

If you noticed, the valueLogger function has an array of values which uses the forEach method to trigger displayValuesWithWait asynchronous function. Also, there is a series of logs generated throughout the code to show what is actually being triggered when the code is executed.

The developer most likely expects the following sequence of logs once the code is run:

Starting to display values
About to run displayValuesWithWait() process for value 1
The current value is: 1
Finished displayValuesWithWait() for value 1
About to run displayValuesWithWait() process for value 2
The current value is: 2
Finished displayValuesWithWait() for value 2
About to run displayValuesWithWait() process for value 3
The current value is: 3
Finished displayValuesWithWait() for value 3
About to run displayValuesWithWait() process for value 4
The current value is: 4
Finished displayValuesWithWait() for value 4
About to run displayValuesWithWait() process for value 5
The current value is: 5
Finished displayValuesWithWait() for value 5
Finished displaying values

If we run the code using the following command (feel free to create a test.js file and execute the code),

node test

we get the following output in the terminal

Result after executing test.js code

Notice how the log “Finished displaying values” is displayed before any of the “The current value is:” logs are displayed.

Why?

One person might say after inspecting the code, the displayValuesWithWait function waits one second to log the value “The current value is:”, suggesting that as the reason the “The current value is:” logs are displayed in the end.

However, using async and await keywords to trigger displayValuesWithWait implies waiting for the asynchronous function to finish prior to continuing to the next process like it is defined inside the forEach callback function.

    console.log('About to run displayValuesWithWait() process for value ', value);

    await displayValuesWithWait(value);

    console.log('Finished displayValuesWithWait() for value ', value);

Even if we modify the displayValuesWithWait function and make it simpler, such as removing the promise definition, the usage of the setTimeout and even the async keyword from the function definition.

function displayValuesWithWait(value) {
  console.log("The current value is: ", value);
}

Notice we haven’t made changes to the forEach callback function yet.

values.forEach(async (value) => {
    console.log('About to run displayValuesWithWait() process for value ', value);

    await displayValuesWithWait(value);

    console.log('Finished displayValuesWithWait() for value ', value);
  });

If we attempt to run the code, this is the result we get:

Result after executing test.js code with modified displayValuesWithWait function

Notice how we still get unexpected results. This time getting all the “Finished displayValuesWithWait() for value” logs in the end.

Therefore, the loop finishes before all the callback function processes finish when using forEach with the async keyword. The await keyword doesn’t wait before executing the next process.

Alternative solutions to running asynchronous code in a loop

Using traditional for loop for sequential executions

Using a traditional for(…;…;…) loop works with asynchronous operations. This solution is recommended if we want to preserve the order of operations in sequential order. We can quickly verify this by tweaking the valueLogger logic to use a for loop.

function displayValuesWithWait(value) {
  console.log("The current value is: ", value);
}

async function valueLogger() {
  const values = [1, 2, 3, 4, 5];

  console.log("Starting to display values");

  for (let i = 0; i < values.length; i++) {
    const value = values[i];
    console.log(
      "About to run displayValuesWithWait() process for value ",
      value
    );

    await displayValuesWithWait(value);

    console.log("Finished displayValuesWithWait() for value ", value);
  }

  console.log("Finished displaying values");
}

valueLogger();

Running the updated logic will output the following logs in the terminal:

The correct expected result after executing test.js code

Finally, we are getting the correct expected result from what we originally intended to execute with our first version of the valueLogger code.

Using for ... of loop for sequential executions

Another option is to use the alternative version of the for loop (for ... of).

function displayValuesWithWait(value) {
  console.log("The current value is: ", value);
}

async function valueLogger() {
  const values = [1, 2, 3, 4, 5];

  console.log("Starting to display values");

  for (const value of values) {
    console.log(
      "About to run displayValuesWithWait() process for value ",
      value
    );

    await displayValuesWithWait(value);

    console.log("Finished displayValuesWithWait() for value ", value);
  }

  console.log("Finished displaying values");
}

valueLogger();

Using Promise.all and Array.map() for concurrent executions (Recommended)

It is possible to use Array.map() method to get an array of promises and execute all promises using Promise.all. Using Promise.all with Array.map() is recommended if we want to concurrently execute asynchronous code for each element in an array. Hence, improving the performance of the code.

function displayValuesWithWait(value) {
  console.log("The current value is: ", value);
}

async function valueLogger() {
  const values = [1, 2, 3, 4, 5]

  console.log("Starting to display values");

  await Promise.all(
    values.map(async (value) => {
      console.log(
        "About to run displayValuesWithWait() process for value ",
        value
      );

      await displayValuesWithWait(value);

      console.log("Finished displayValuesWithWait() for value ", value);
    })
  );

  console.log("Finished displaying values");
}

valueLogger();

Checking the logs from running the previous example will help us understand why the code is run concurrently.

Result after using Promise.all and array.map function

Notice how the logs are not in sequential order. If the logs were in sequential order, we would have seen a result like this:

About to run displayValuesWithWait() process for value  1
The current value is:  1
Finished displayValuesWithWait() for value  1
About to run displayValuesWithWait() process for value  2
The current value is:  2
Finished displayValuesWithWait() for value  2

To make it even more clear the “concurrent” concept, we are tweaking the displayValuesWithWait function to use a timeout at random times.

async function displayValuesWithWait(value) {
  // use the alternative wait to explain concurrent
  const wait = Math.floor(Math.random() * 2) * 1000;

  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("The current value is: ", value);
      resolve();
    }, wait);
  });
}

async function valueLogger() {
  const values = [1, 2, 3, 4, 5];

  console.log("Starting to display values");

  await Promise.all(
    values.map(async (value) => {
      console.log(
        "About to run displayValuesWithWait() process for value ",
        value
      );

      await displayValuesWithWait(value);

      console.log("Finished displayValuesWithWait() for value ", value);
    })
  );

  console.log("Finished displaying values");
}

valueLogger();

After running the code, notice how the values of the array are not logged in the correct order. However, the processes executed per each element of the array finish before displaying the “Finished displaying values” log.

Another example of Promise.all with array.map showing the concurrent behavior

Note: Using the map function by itself won’t allow the asynchronous code to wait when using the await keyword. The following example will behave similarly to how asynchronous code is written using the forEach function.

function displayValuesWithWait(value) {
  console.log("The current value is: ", value);
}

async function valueLogger() {
  const values = [1, 2, 3, 4, 5];

  console.log("Starting to display values");

  values.map(async (value) => {
    console.log(
      "About to run displayValuesWithWait() process for value ",
      value
    );

    await displayValuesWithWait(value);

    console.log("Finished displayValuesWithWait() for value ", value);
  });

  console.log("Finished displaying values");
}

valueLogger();

The result from running the previous snippet of code is not what we are expecting.

Result after executing test.js code using the array.map function

Conclusion

All in all, JavaScript forEach function executes code synchronously regardless of using it with or without the async and await keywords, which are meant to run code asynchronously. Using forEach with asynchronous code doesn’t mean the code will not run. However, it will run with unexpected behaviors.

Fortunately, there are solutions to run asynchronous code for all the items of an array such as using traditional JavaScript loops (for (...;...;...) or for ... of) for sequential executions, or combining Promise.all with array.map() for concurrent executions.

Did you like this article?

Share your thoughts by replying on Twitter of Become A Better Programmer or to my personal Twitter account.