DEV Community

Husnain Mustafa
Husnain Mustafa

Posted on • Updated on

Why we cant use await inside forEach? and its workaround.

Have you ever tried using the await inside forEach?

[].forEach(async(item) => {
    await doSomethingWith(item)
})
Enter fullscreen mode Exit fullscreen mode

Well I have, when I didn't know much about async/await and forEach. But didn't have the expected result. To understand this, we must first understand how async/await works.

How async/await works:

async/await is the another way of doing .then on the Promises. So whenever an async function reaches the statement where await is used and Promise is yet to be resovled, that function is then runs separately from the parent function, unless we use await on the child function. Let me give you an example:

const functionA = async () => {
  console.log("Starting Function A");
  await new Promise((resolve) => {
    setTimeout(resolve, 300);
  });
  console.log("Ending Function A");
};

const myFunction = () => {
  console.log("1");
  functionA();
  console.log("2");
};
myFunction();
Enter fullscreen mode Exit fullscreen mode

The above code logs:

1
Starting Function A
2
Ending Function A
Enter fullscreen mode Exit fullscreen mode

You see, first 1 is logged, and then we go inside functionA, logs Starting Function A and as soon as we hit await keyword, functionA runs separately and myFunction moves on and logs 2 and finally when our promise resolves in 300 ms, we get the log of Ending Function A.

Lets give it another try with using await on functionA:

const functionA = async () => {
  console.log("Starting Function A");
  await new Promise((resolve) => {
    setTimeout(resolve, 300);
  });
  console.log("Ending Function A");
};

const myFunction = async () => {
  console.log("1");
  await functionA();
  console.log("2");
};
myFunction();
Enter fullscreen mode Exit fullscreen mode

Now we get the logs:

1
Starting Function A
Ending Function A
2
Enter fullscreen mode Exit fullscreen mode

In this case, we must complete the execution of functionA to move on. But you notice that out myFunction also becomes async. So it will also runs separately from parent function or global scope.

Let's log another thing just after calling myFunction:

const functionA = async () => {
  console.log("Starting Function A");
  await new Promise((resolve) => {
    setTimeout(resolve, 300);
  });
  console.log("Ending Function A");
};

const myFunction = async () => {
  console.log("1");
  await functionA();
  console.log("2");
};
myFunction();
console.log("3");
Enter fullscreen mode Exit fullscreen mode

Can you guess what will be logged?
Here is our logs:

1
Starting Function A
3
Ending Function A
2
Enter fullscreen mode Exit fullscreen mode

You see, our code runs synchronously until we hit the await statement where Promise is yet to be resolved. So when we called myFunction, our code ran synchronously and logged 1 and then it went inside the functionA, here it logged Starting Function A, and then we hit the await statement where we have to wait for the Promise to resolve, so both of our functions, functionA and myFunction runs separately. And our code pointer comes to log 3. And as soon as promise resolves, it logs Ending Function A and then 2

Now it might occur to you why we cannot use async/await inside forEach.
Let's see the forEach function first:

const forEach = (callback)=>{
    ...
    ...
    callback()
    ...
    ...
}
Enter fullscreen mode Exit fullscreen mode

Now when we do:

myArray.forEach(async(item) => {
    await doSomethingWith(item)
})
Enter fullscreen mode Exit fullscreen mode

As soon as our code hit await statement, our doSomethingWith function runs asynchoronously and our code pointer comes back execute the next line in forEach and hence, it completes the whole forEach and our await statements are still yet to be resolved.

Let's see an example here:

const promiseFunction = async (item) => {
  await new Promise((resolve) => {
    setTimeout(resolve, 600);
  });
};

const myArray = [1, 2, 3, 4, 5, 6, 7, 8];

const myFunction = () => {
  console.log("Starting my Function");
  myArray.forEach(async (item) => {
    console.log("Starting forEach with item value", item);
    await promiseFunction(item);
    console.log("Ending forEach with item value", item);
  });
  console.log("Ending my Function");
};

myFunction();
Enter fullscreen mode Exit fullscreen mode

And here is our logs:

Starting my Function
Starting forEach with item value 1
Starting forEach with item value 2
Starting forEach with item value 3
Starting forEach with item value 4
Starting forEach with item value 5
Starting forEach with item value 6
Starting forEach with item value 7
Starting forEach with item value 8
Ending my Function
Ending forEach with item value 1
Ending forEach with item value 2
Ending forEach with item value 3
Ending forEach with item value 4
Ending forEach with item value 5
Ending forEach with item value 6
Ending forEach with item value 7
Ending forEach with item value 8
Enter fullscreen mode Exit fullscreen mode

You see it starts the next callback without ending the previous, that is because each callback is waiting for its await to be resolved. Hence we get to the end of the myFunction but could not complete callback functions.

The workaround:

We can use await Promise.all with map function to have await for each of the callbacks. Here is how:

const promiseFunction = async (item) => {
  console.log("Starting promiseFunction with item value", item);
  await new Promise((resolve) => {
    setTimeout(resolve, 600);
  });
  console.log("Ending promiseFunction with item value", item);
};

const myArray = [1, 2, 3, 4, 5, 6, 7, 8];

const myFunction = async () => {
  console.log("Starting my Function");
  await Promise.all(
    myArray.map((item) => {
      return promiseFunction(item);
    })
  );
  console.log("Ending my Function");
};

myFunction();

Enter fullscreen mode Exit fullscreen mode

We are simply making the array of Promises by:

myArray.map((item) => {
  return promiseFunction(item);
})
Enter fullscreen mode Exit fullscreen mode

promiseFunction returns a function so by map funtion, we have array of Promises. And we finally resolve all the promises by: await Promise.all.

And here is our logs:

Starting my Function
Starting promiseFunction with item value 1
Starting promiseFunction with item value 2
Starting promiseFunction with item value 3
Starting promiseFunction with item value 4
Starting promiseFunction with item value 5
Starting promiseFunction with item value 6
Starting promiseFunction with item value 7
Starting promiseFunction with item value 8
Ending promiseFunction with item value 1
Ending promiseFunction with item value 2
Ending promiseFunction with item value 3
Ending promiseFunction with item value 4
Ending promiseFunction with item value 5
Ending promiseFunction with item value 6
Ending promiseFunction with item value 7
Ending promiseFunction with item value 8
Ending my Function
Enter fullscreen mode Exit fullscreen mode

Now we can see, although each promiseFunction runs asynchronously, but we ensure the our myFunction ends only when our all the Promises get resolved.

Hope you learned something today.
Happy Learning ❤️

Top comments (4)

Collapse
 
kurealnum profile image
Oscar

Awesome article! If you were curious, you can add syntax highlighting to your code snippets by adding the language after the opening backticks like so:

Image description

which produces

console.log("Hello World!")
Enter fullscreen mode Exit fullscreen mode
Collapse
 
aminnairi profile image
Amin

You can also create your own helper function that does a similar job compared to Array.prototype.forEach. Here using a function declaration to not pollute the Array.prototype.

async function forEachAsync(array, asyncCallback) {
  if (!Array.isArray(array)) {
    throw new Error("First argument should be an array");
  }

  if (typeof asyncCallback !== "function") {
    throw new Error("Second argument should be a function");
  }

  let index = 0;

  for await (const item of array) {
    await asyncCallback(item, index++, array);
  }
}

const urls = [
  "https://jsonplaceholder.typicode.com/users",
  "https://jsonplaceholder.typicode.com/posts",
  "https://jsonplaceholder.typicode.com/photos",
];

try {
  forEachAsync(urls, async (url, index) => {
    const response = await fetch(`${url}/${index + 1}`);
    const json = await response.json();

    console.log(json);
  });
} catch (error) {
  console.error(error);
}
Enter fullscreen mode Exit fullscreen mode

Not as sexy as polluting the Array.prototype but it does the trick as expected.

Collapse
 
pavelee profile image
Paweł Ciosek

Thank you! 🙏

Collapse
 
raguay profile image
Richard Guay

Been there and done that!