loading...

Breaking down confusion of combining Async/Await with Array.forEach()

dinos_vl profile image Dinos Vlachantonis ・3 min read

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:

Awesome corgi loading...

If you find any mistake do not hesitate to leave a comment.
Any feedback is welcome :)

Discussion

pic
Editor guide
Collapse
jdforsythe profile image
Jeremy Forsythe

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

Collapse
dinos_vl profile image