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:
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.
- Use the
reduce()
method - Use the
for...of
loop - Use the
Promise.all()
method
Table Of Contents
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.
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.
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.
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.
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()
.
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.
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()
.
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:
- Unexpected identifier Error in JavaScript
- Dynamically Access Object Property Using Variable in JavaScript
- Check if a Key exists in an Object in JavaScript
- Replace All String Occurrences in JavaScript
- Get the Last N Elements of an Array in JavaScript
- Calculate Percentage Between Two Numbers in JavaScript
- Remove Special Characters from a String in JavaScript
- Wait for all promises to resolve in JavaScript