Alternatives to Using Async/Await with a forEach loop

If you’re like most JavaScript developers, you’ve probably used the good old forEach loop at some point. It’s a great way to iterate over an array of items, and it works well with synchronous operations.


Problem

However, there is one potential pitfall that you need to be aware of when using forEach with asynchronous operations.

Let’s take a look at an example to see what I mean.

Suppose we have an array of milliseconds and want to use the forEach loop to asynchronously proceed each item using that specific timeframe. We could do something like this:

JavaScript
const milliseconds = [400, 200, 100, 300];
milliseconds.forEach(async (ms) => {
  await setTimeout(() => {
    console.log(ms);
  }, ms);
});

// Expected Output 👉 400 200 100 300
// Actual Output 👉 100 200 300 400

It prints the result in sequence 100 200 300 400, but we expected it to print in the same order we provided, which is 400 200 100 300.

The reason is that the forEach loop does not wait for the asynchronous operation to complete before moving on to the next iteration.

Another issue is that if any promise throws an error, the error won’t be caught because promises are not getting handled inside the forEach loop.


Solution

To overcome these issues, we can use the following solutions.

  1. Use the reduce() method
  2. Use the for...of loop
  3. Use the Promise.all() method

1. Use the for…of loop

The for…of loop with async await is a great way to ensure that asynchronous operations are performed in the right order.

The for…of loop allows you to iterate over a collection of promises and ensures that the next operation in the loop won’t start until the previous one has finished.

JavaScript
const milliseconds = [400, 200, 100, 300];
for (const ms of milliseconds) {
  await new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("milliseconds", ms);
      resolve(ms);
    }, ms);
  });
}
// Promise gets resolved in serial order
// milliseconds 400
// milliseconds 200
// milliseconds 100
// milliseconds 300

It works as expected.

⚠️ However, there is one issue: it requires a large polyfill. So it may cause issues while working with large datasets.

A good alternative is to use the reduce() method.


2. Use the reduce() method

The reduce() method has the same behavior as the for...of loop but slightly harder to read.

JavaScript
const milliseconds = [400, 200, 100, 300];
milliseconds.reduce(async (a, ms) => {
  // Wait for the previous item to finish processing
  await a;
  // Process this item
  await new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("milliseconds", ms);
      resolve(ms);
    }, ms);
  });
}, Promise.resolve());

// Promise gets resolved in serial order
// milliseconds 400
// milliseconds 200
// milliseconds 100
// milliseconds 300

We used the accumulator a not as a total or a summary but just as a way to pass the promise from the previous item’s callback to the next item’s callback so that we can wait for the previous item to finish being processed.

If the order doesn’t matter to you, you can use Promise.all() method.


3. Use the Promise.all() method

The Promise.all() is faster than the reduce() method and the for...of loop as it processes all the promises parallelly.

All you need to do is pass an array of promises to the Promise.all(), which will return results as a single Promise.

JavaScript
const milliseconds = [400, 200, 100, 300];
const promises = milliseconds.map(ms => new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log("milliseconds", ms);
    resolve(ms);
  }, ms);
}));

console.log(promises); // 👉 [Promise, Promise, Promise, Promise]

const res = await Promise.all(promises);

// The promises get resolved parallelly
// milliseconds 100
// milliseconds 200
// milliseconds 300
// milliseconds 400

console.log(res); // 👉[400, 200, 100, 300]

Let’s see how to use it.

1. Create array of Promises.

JavaScript
const milliseconds = [400, 200, 100, 300];
const promises = milliseconds.map(ms => new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log("milliseconds", ms);
    resolve(ms);
  }, ms);
}));

console.log(promises); // 👉 [Promise, Promise, Promise, Promise]

2. Pass array of Promises to the Promise.all().

JavaScript
const res = await Promise.all(promises);

// The promises get resolved parallelly
// milliseconds 100
// milliseconds 200
// milliseconds 300
// milliseconds 400

console.log(res); // 👉[400, 200, 100, 300]

As you can see above, promises get resolved parallelly but the Promise.all() return array of resolved Promise in the same order passed.

It also allows you to handle errors better since you can check the results of all promises in one go.

JavaScript
try {
  const p1 = Promise.resolve('200');
  const p2 = Promise.reject('Rejected');
  const p3 = new Promise((resolve, reject) => {
    setTimeout(resolve, 100, 'resolved');
  });

  const res = await Promise.all([p1, p2, p3]);
} catch(err){
  // 👇 this runs
  console.log('Error', err)
}

Here, our second Promise got rejected so Promise.all() terminates the process and goes to catch block.

⚠️ As you can see, the Promise.all() method terminates the process if any promise gets rejected.

However, sometimes, we must wait until all promises are completed, even if some are rejected.

In such cases, use the Promise.allSettled() method works the same way as Promise.all().

JavaScript
const p1 = Promise.resolve('200');
const p2 = Promise.reject('Rejected');
const p3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'resolved');
});


const settledPromises= await Promise.allSettled([p1, p2, p3]);
console.log(settledPromises);
// 👇️ output
// [
//   { status: 'fulfilled', value: '200' },
//   { status: 'rejected', reason: 'Rejected' },
//   { status: 'fulfilled', value: 'resolved' }
// ]

Our second promise was rejected, but still, it proceed all the promises and returned an array of objects.

For each result object, a status string is present. If the status is fulfilled, then a value is present. If the status is rejected, then a reason is present.

The value (or reason) reflects what value each promise was fulfilled (or rejected) with.


Conclusion

In conclusion, I would suggest not use the forEach() loop with async/await; instead, you can use the for...of loop, reduce() method, Promise.all() or Promise.allSettled().


Learn More:

Unlock your programming potential with Stack Thrive! Discover step-by-step tutorials, insightful tips, and expert advice that will sharpen your coding skills and broaden your knowledge.

Leave a Comment

Facebook Twitter WhatsApp