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.
Table of Contents
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
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:
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:
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.
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.
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.
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.