DEV Community

Arnaud
Arnaud

Posted on

Don't await in loops

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!

Latest comments (7)

Collapse
 
jalal246 profile image
Jalal πŸš€ • Edited

async function doesn't work inside for loop

for (let i = 0; i < array.length; i++) {
  // it won't wait here. 
  const result = await work(array[i]);

  // result is an unresolved promise.
  doSomething(result);
}

Instead, you push all promises in an array then resolve them with Promise.all

Collapse
 
karataev profile image
Eugene Karataev

Actually, async works as expected in for loop.

function work(n) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(n * 10);
    }, 200);
  })
}

(async function() {
  array = [1,2,3];
    for (let i = 0; i < array.length; i++) {
    // it WILL wait here. 
    const result = await work(array[i]);

    // result is 10, 20, 30 as expected
    console.log(result);
  }
})();
Collapse
 
arnaud profile image
Arnaud

Yes, that's exactly it!

Collapse
 
pentacular profile image
pentacular

However, this is far from optimal, the whole point of asynchronous operations is that they can be done in parallel.

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.

Collapse
 
arnaud profile image
Arnaud

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.

Collapse
 
pentacular profile image
pentacular

I think you're still making incorrect claims about parallelism.

Now the entire loop will be as slow as its slowest operation, in our case this is ~1 second, 5.5x faster!

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.

Thread Thread
 
arnaud profile image
Arnaud • Edited

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.