Back

Francesco Marassi

Full-Stack developer & UI/UX designer.

Why isn't async/await working in a .forEach cycle?

April 22, 2020

img

Let's face it: since the introduction of the async/await pattern, we have tried to use it every where.

Long gone and (almost) forgotten are the days of big chains of javascript callbacks and Promises.resolve: now all the functions starts with an async, even if it won't contain asynchronous code... just in case :)

But some parts of Javascript are not yet ready to work out of the box with this pattern. One of these parts is the .forEach. (other methods that don't go together with async/await are .map, .filter and .reduce, but I will write another article on these)

What is .forEach?

.forEach is an array method that permits to call a function on every element of the array. Easy right?

So this is an example of .forEach:

console.log("Start");
const foodArray = ["orange", "salami", "salmon", "kale", "banana"];

foodArray.forEach((food) => {
  console.log(`${food} is one of my favorite food!`);
});

console.log("End");

If I save this script in a file food.js and then I run node food.js, this will be the output:

Screenshot 2020-04-21 at 22.45.20

So everything is correct: the function was called on every element of the array and the order of the array matches the order of the console.log output. Yey! ✨

But what happens if the function is more complex than a single console.log?

Introducing async/await

The code that we wrote in the previous section is great, but I want more. I want to know the class (meat, fish, vegetable, fruit) of my favorite foods.

Let's say that I have created in javascript some complex algorithm that, given in input a food's name, is able to return the class. Pretty great, eh? It's my best code ever. 🚀

This algorithm uses lots of calculus power and DB resources, so it will return a promise.

Obviously I'm not able to share with you this incredible code with you here (and no, I won't respond to any indiscretion that this algorithm doesn't exist), so for the sake of this article I will use a fake function that mimics that.

const AIFoodRecognition = (food) => {
  const dictionary = {
    orange: "fruit",
    salami: "meat",
    salmon: "fish",
    kale: "vegetable",
    banana: "fruit",
  };

  return new Promise((r) =>
    setTimeout(() => {
      return r(`${food} is a ${dictionary[food]}`);
    }, 500)
  );
};

To call this function, I will use a awaitinside my .forEach. Let's change a little the previous code:

const foodArray = ["orange", "salami", "salmon", "kale", "banana"];

const run = async () => {
  console.log("Start")
  
  foodArray.forEach(async (food) => {
    const output = await AIFoodRecognition(food);
    console.log(output);
  });
  
  console.log("End")
};


run()

Since it's not yet standard to use async await in a global state, I created a function run that wraps the code. Inside the .forEach function (now an async function) we call AIFoodRecognition with await and we print the result.

Easy, right? The result should begin with "Start", then will prints all my favourite food classes, and then will print "End".

Let's try it:

Screenshot 2020-04-21 at 23.01.45

Well... not exactly what we expected. That is because .forEach only invokes the function, it doesn't wait it to end and every AIFoodRecognition function is expected to end after at least 500ms, way more than the time the main thread will spent to finish the .forEach cycle and print End. That's a bummer!

So what can we do to fix this?

Fortunately there are some solutions to this problem.

  • rewrite .forEach to support await/async
  • use for() cycle
  • use Promise.all

Let's start with the least convenient, even if it seems the best one

Rewrite .forEach to support await/async

This seems like a great solution! Let's find the code for the .forEach polyfill, let's wrap everything in a async function and then... Profit! 💰💰💰

This is a simple .forEach polyfill:

Array.prototype.forEach = function forEach(callback, thisArg) {
  if (typeof callback !== "function") {
    throw new TypeError(callback + " is not a function");
  }
  var array = this;
  thisArg = thisArg || this;
  for (var i = 0, l = array.length; i !== l; ++i) {
    callback.call(thisArg, array[i], i, array);
  }
};

Let's change it to support async/await:

Array.prototype.forEach = async function forEach(callback, thisArg) {
  if (typeof callback !== "function") {
    throw new TypeError(callback + " is not a function");
  }
  var array = this;
  thisArg = thisArg || this;
  for (var i = 0, l = array.length; i !== l; ++i) {
    await callback.call(thisArg, array[i], i, array);
  }
};

As you can see, we added an async before the function in line 1 and then before callback.call we will use await. We are going to replace the forEach method in Array.prototype with this method.

Ok! Now let's try to use it in our previous code:

const run = async () => {
  console.log("Start");
  foodArray.forEach(async (food) => {
    const output = await AIFoodRecognition(food);
    console.log(output);
  });
  console.log("End");
};

And after running again, the output will be...

Screenshot 2020-04-21 at 23.24.05

OUCH.

That's because we forgot something important: now forEach is an async function, so to wait for his end we need to use await before calling it.

If we just add await before the forEach, like this

...
await foodArray.forEach(async (food) => {
...

Screenshot 2020-04-21 at 23.26.41

YESSSS ✨

So we should only add await everywhere we already used .forEach and wrap it in an async function... not worth it. Also, it's a bad practice to override some default class methods, since in the future you could forget about this override or other developers can jump inside the project and spend hours or days trying to find the bug

Let's try to find some other solutions, maybe less hackish.

Use our good old for() cycle

The revenge of the old for() cycle. We replace it everywhere in the past with the new better-looking .forEach, and now we return asking forgiveness.

Let's try to use it in our code:

const run = async () => {
  console.log("Starting...");
  for (let i = 0; i <= foodArray.length; i++) {
    const output = await AIFoodRecognition(foodArray[i]);
    console.log(output);
  }
  console.log("Done!");
};

And... it's working at our first try!

Kapture 2020-04-22 at 10.47.17

But I think there is something even better than this, that maybe can help us reduce our waiting time.

Promise.all

Until now to end our program we waited 3 seconds (500 ms for every element in the foodArray). That's because the await was inside the cycle. But what if we had 100 elements in foodArray? Or 1000? The waiting time would be terrible.

Maybe there is a way to do all the food recognition in parallel instead of waiting for each one to finish before starting the next one? Yes, there is: it's called Promise.all. This method of Promise accepts an array as parameter and will return only when all the elements inside the array has completed, returning a new array of results.

We are going to change the code a little but I promise you it will worth it:

const run = async () => {
  console.log("Starting...");
  const promises = [];
  foodArray.forEach((food) => {
    promises.push(AIFoodRecognition(food));
  });
  const outputs = await Promise.all(promises);
  outputs.forEach((result) => console.log(result));
  console.log("Done!");
};

So now we have a new array called promises, and inside the .forEach we are going to add every AIFoodRecognition function call, in this way we start every recognition in parallel. Remember: AIFoodRecognition returns a Promise, so we can't still get our classes from this array. To obtain that, we need to use Promise.all with a single await, that will return a new array that contains the returns of every element of our promises array. Now we only need to cycle this array to print the results and we are done! ✨

Let see the difference between the code with Promise.all and with the for() cycle (I setup a delay of 1500ms instead of 500ms for these gifs)

Kapture 2020-04-22 at 11.14.12

As you can see, the Promise.all method is way faster than the for() cycle. So if you don't need something from the output result of the previous cycle, you should always use the Promise.all version to run some async/await code in a cycle.

Let me know what you think or if you have other ways to use await inside a cycle, you can always find me on Twitter 💛