Don't await in loops
Arnaud
Posted on August 3, 2020
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!
Posted on August 3, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.