for await of VS Promise.all

25,238

Solution 1

Yes, they absolutely are different. for await is supposed to be used with asynchronous iterators, not with arrays of pre-existing promises.

Just to make clear,

for await (const res of items.map(e => somethingAsync(e))) …

works the same as

const promises = items.map(e => somethingAsync(e));
for await (const res of promises) …

or

const promises = [somethingAsync(items[0]), somethingAsync(items[1]), …];
for await (const res of promises) …

The somethingAsync calls are happening immediately, all at once, before anything is awaited. Then, they are awaited one after another, which is definitely a problem if any one of them gets rejected: it will cause an unhandled promise rejection error. Using Promise.all is the only viable choice to deal with the array of promises:

for (const res of await Promise.all(promises)) …

See Waiting for more than one concurrent await operation and Any difference between await Promise.all() and multiple await? for details.

Solution 2

The need for for await ... arises when on an asynchronous iterator the computation of the current iteration depends on some of the previous iterations. If there are no dependences, Promise.all is your choice. The for await construct was designed to work with asynchronous iterators, although - as in your example, you can use it with an array of promises.

See the example paginated data in the book javascript.info for an example using an asynchronous iterator that can't be rewritten using Promise.all:

(async () => {
  for await (const commit of fetchCommits('javascript-tutorial/en.javascript.info')) {
    console.log(commit.author.login);
  }
})();

Here the fetchCommits async iterator makes a request to fetch the commits of a GitHub repo. The fetch responds with a JSON of 30 commits, and also provides a link to the next page in the Link header. Therefore the next iteration can only start after the previous iteration has the link for the next request

async function* fetchCommits(repo) {
  let url = `https://api.github.com/repos/${repo}/commits`;

  while (url) {
    const response = await fetch(url, { 
      headers: {'User-Agent': 'Our script'}, 
    });

    const body = await response.json(); // (array of commits

    // The URL of the next page is in the headers, extract it using a regexp
    let nextPage = response.headers.get('Link').match(/<(.*?)>; rel="next"/);
    nextPage = nextPage?.[1];

    url = nextPage;

    for(let commit of body) { // yield commits one by one, until the page ends
      yield commit;
    }
  }
}

Solution 3

As you said Promise.all will send all the requests in one go and then you will get the response when all of them gets completed.

In the second scenario, you will send the request in one go but recieve response as for each one by one.

See this small example for reference.

let i = 1;
function somethingAsync(time) {
  console.log("fired");
  return delay(time).then(() => Promise.resolve(i++));
}
const items = [1000, 2000, 3000, 4000];

function delay(time) {
  return new Promise((resolve) => { 
      setTimeout(resolve, time)
  });
}

(async() => {
  console.time("first way");
  const promises = await Promise.all(items.map(e => somethingAsync(e)));
  for (const res of promises) {
    console.log(res);
  }
  console.timeEnd("first way");

  i=1; //reset counter
  console.time("second way");
  for await (const res of items.map(e => somethingAsync(e))) {
    // do some calculations
    console.log(res);
  }
  console.timeEnd("second way");
})();

You could try it here as well - https://repl.it/repls/SuddenUselessAnalyst

Hope this helps.

Solution 4

Actually, using the for await syntax does fire the promises all at once.

The small piece of code proves it:

const sleep = s => {
  return new Promise(resolve => {
    setTimeout(resolve, s * 1000);
  });
}

const somethingAsync = async t => {
  await sleep(t);
  return t;
}

(async () => {
  const items = [1, 2, 3, 4];
  const now = Date.now();
  for await (const res of items.map(e => somethingAsync(e))) {
    console.log(res);
  }
  console.log("time: ", (Date.now() - now) / 1000);
})();

stdout: time: 4.001

But the inside of the loop doesn't act "as a callback". If I reverse the array, all the logs appear at once. I suppose that the promises are fired at once and the runtime just waits for the first one to resolve to go to the next iteration.

EDIT: Actually, using for await is bad practice when we use it with something other than an asynchronous iterator, best is to use Promise.all, according to @Bergi in his answer.

Share:
25,238
NicoAdrian
Author by

NicoAdrian

Updated on January 13, 2021

Comments

  • NicoAdrian
    NicoAdrian over 3 years

    Is there any difference between this:

    const promises = await Promise.all(items.map(e => somethingAsync(e)));
    for (const res of promises) {
      // do some calculations
    }
    

    And this ?

    for await (const res of items.map(e => somethingAsync(e))) {
      // do some calculations
    }
    

    I know that in the first snippet, all the promises are fired at the same time but I'm not sure about the second. Does the for loop wait for the first iteration to be done to call the next promise ? Or are all the promises fired at the same time and the inside of the loop acts like a callback for them ?