In software development, a very common task is to perform an operation on each element of an iterable:
for (let i = 0; i < array.length; i++) {
work(array[i]);
}
When introducing asynchronous operations, it's easy to write:
for (let i = 0; i < array.length; i++) {
const result = await work(array[i]);
doSomething(result);
}
However, this is far from optimal as each successive operation will not start until the previous one has completed.
Imagine the work
function takes 100ms more than its previous execution:
const work = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
for (let i = 0; i < 10; i += 1) {
await work((i + 1) * 100);
}
The first call will take 100ms, the second 200ms, and so on. The entire loop will be equal to the sum of the duration of each asynchronous operation, about 5.5 seconds!
Since work
is asynchronous, we can use Promise.all
to execute all the operations in parallel. All we need to do is add the promises to an array and call Promise.all
const p = [];
for (let i = 0; i < 10; i += 1) {
p.push(work((i + 1) * 100));
}
const results = await Promise.all(p);
Now the entire loop will be as slow as its slowest operation, in our case this is ~1 second, 5.5x faster!
However, Promise.all does have one problem. It will reject immediately upon any of the input promises rejecting.
try {
const p = [
new Promise((resolve, reject) => setTimeout(() => reject(new Error('Failure!')), 200)),
];
for (let i = 0; i < 10; i += 1) {
p.push(work((i + 1) * 100));
}
const results = await Promise.all(p);
console.log(results); // <- never executed !
} catch (error) {
console.error(error.message); // Error: Failure!
}
There is only one little change needed to get all promises to run. We need to add a catch
to each promise to handle the failures of the failed promises, and let the other promises resolve.
const errorHandler = (e) => e;
const p = [
new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('Failure!')), 200);
}).catch(errorHandler),
];
for (let i = 0; i < 10; i += 1) {
p.push(work((i + 1) * 100).catch(errorHandler));
}
const results = await Promise.all(p);
results.forEach((result, i) => {
if (result instanceof Error) {
console.log(`Call ${i} failed`);
return;
}
console.log(`Call ${i} succeeded`);
});
Notice how a new Error
is passed to the reject callback. The handler simply returns the error, but this would also be a good place to log it. If in the handler the error were thrown instead, then we would default back to the previous behavior where the catch block would be entered.
We should always do our best to execute asynchronous operations in parallel, and we've seen that with proper error handling, it is possible to use Promise.all
to execute a bunch of promises in parallel, even if one fails.
Happy coding!
Top comments (7)
async function doesn't work inside
for loop
Instead, you push all promises in an array then resolve them with
Promise.all
Actually,
async
works as expected in for loop.Yes, that's exactly it!
async operations in javascript cannot be done in parallel -- they can only be interleaved.
The purpose of async is to allow another operation to occur when one is blocked.
For this reason, there is absolutely nothing wrong with using async in loops -- it is perfectly reasonable to have async operations with a required ordering.
Of course, it's nice to avoid unnecessary ordering constraints on operations -- and this can give you more alternative things to do when one is blocked.
Thank you for your comment. You are correct, sometimes async operations need to be done in a given order. The point I am looking to make in the article is to use catch blocks when the behavior of Promise.all rejecting immediately upon any of the input promises rejecting is not desired.
I think you're still making incorrect claims about parallelism.
No, the entire loop will be at least as slow as the sum of all of its operations.
It's just that in this particularly contrived example, the operations don't do any work -- they just sit around doing nothing for a while -- so this is all dead-time.
The dead-time can be 'parallelized' because no work occurs, but the over-all story you're making in the article seems very misleading to me.
The 'contrived example' is to illustrate the purpose of this lint rule: eslint.org/docs/rules/no-await-in-...
Some people disable this rule, not because they need to run operations in order, but because they don't know how to disable the fail fast behavior of Promise.all as explained at the end of this page: developer.mozilla.org/en-US/docs/W... under "Promise.all fail-fast behaviour".
Apparently I am not doing a good job at explaining it, thanks for the feedback, I've made some tweaks based on your observations.