Last week I was having a normal day at work when I suddenly stumbled upon something that really confused me. I was trying to loop an array and call an async function for each element. Yet, the result I was getting was not what I expected.
A dummy version of the situation I had could be:
const names = ['George', 'Margie', 'Anna']
const delay = () => new Promise(resolve => setTimeout(resolve, 3000))
names.forEach(async (name) => {
await delay()
console.log(`Greetings to you ${name}`)
})
console.log('farewell')
By simply running this in node
we get the following result:
$ node awaitForEach.js
farewell
Greetings to you George
Greetings to you Margie
Greetings to you Anna
What? Wait a second...
That was not what I would expect to see. We definitely have an await
when we are calling delay
and Array.prototype.forEach
is a synchronous function, so I would be quite confident that the greetings should appear before the farewell
is printed in the console.
A deep look on Array.prototype.forEach
That can get quite very confusing, until you actually take a look at how Array.prototype.forEach
is implemented.
A simplified version would be:
Array.prototype.forEach = function(callback, thisArg) {
const array = this
thisArg = thisArg || this
for (let i = 0, l = array.length; i !== l; ++i) {
callback.call(thisArg, array[i], i, array)
}
}
As you can see, when we are calling the callback function, we are not waiting for it to finish.
That means, waiting for our delay()
function to finish is not enough when Array.forEach()
is not waiting for our callback to finish as well!
Let's try again
Alright, now we could solve this in many ways. But let's try to fixing the issue in the actual Array.forEach()
.
Let's write our own asyncForEach
!
We just need to make the loop wait for the callback to finish before moving forward to the next element.
Array.prototype.asyncForEach = async function(callback, thisArg) {
thisArg = thisArg || this
for (let i = 0, l = this.length; i !== l; ++i) {
await callback.call(thisArg, this[i], i, this)
}
}
Then let's try our previous scenario. Now instead of Array.prototype.forEach
we are going to use our own Array.prototype.asyncForEach
.
(Note that we wrapped our code into a greetPeople()
function, since we now need to await
for the asyncForEach()
, which can only be inside an async
function.)
const greetPeople = async (names) => {
const delay = () => new Promise(resolve => setTimeout(resolve, 3000))
await names.asyncForEach(async (name) => {
await delay()
console.log(`Greetings to you ${name}`)
})
console.log('farewell')
}
greetPeople(['George', 'Margie', 'Anna'])
And as we all expect, if we now run our updated code the outcome is the one that we desire.
$ node awaitForEach.js
Greetings to you George
Greetings to you Margie
Greetings to you Anna
farewell
We made it!
We have our own async-friendly forEach
array implementation.
Note that we could have the same behavior with other popular Array
functions like Array.map
or Array.filter
.
Now I have to admit, this is probably not always going to be the best way to solve the issue.
But this is a great way understanding a bit better how Array.forEach
actually works and in what scenarios it can get a bit problematic/confusing.
Meme award section
Well, if you are reading this it means that you actually read the whole thing, wow!
Your award is this nice corgi picture:
If you find any mistake do not hesitate to leave a comment.
Any feedback is welcome :)
Top comments (2)
It is for this reason that we prefer for..of over .forEach when working with async. In most cases you're iterating an array and doing something async, you probably want .map() and Promise.all() but in instances you really do want the .forEach() behavior, use for..of instead
Totally agree!