If you await a Promise.all with an array of the promises, it will take approximately 1000ms.
In summary, using individual awaits runs them serially, while Promise.all runs them concurrently.
If you’re doing CPU bound work without workers, it doesn’t make much of a difference, but if you’re doing I/O bound tasks, like HTTP requests, then doing it in parallel will likely make a significant difference.
https://codepen.io/tomtheisen/pen/QWPOmjp
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function test() {
const start = new Date;
const promises = Array(3).fill(null).map(() => delay(1000));
for (const p of promises) await p;
const end = new Date;
console.log("elapsed", end - start); // shows about 1010
}
test(); for (var i = 0; i < 3; i++) {
await delay(1000);
}
However (as you are possibly well aware), this line in your example is starting all the work immediately and essentially in parallel: const promises = Array(3).fill(null).map(() => delay(1000));
So the timing of your for...of loop is that the first element probably takes about 1000ms to complete, and then the other two seem to happen instantly.Promise.all is just an alternative to writing the for...of await loop:
await Promise.all(promises);
I guess it relies on you already being familiar with the Promise API, but I feel that Promise.all() has slightly less cognitive load to read and its intent is more immediately clear.A strong case for preferring the Promise.all() is that Promise.allSettled(), Promise.any() and Promise.race() also exist for working with collections of promises, and unlike Promise.all(), they would not be so easily reproduced with a one liner for...of loop, so its not unreasonable to expect that JS developers should be aware of Promise.all(), meaning there is no reason for it not to be the preferred syntax for the reasons I stated above.
Promise.allSettled has a poor-mans implementation too. But the others really don't have such a thing.
My impression is that Promise.all() is kind of nice, but it's really not that big a deal or important. If it didn't exist, you could get the same happy-path code behavior without really even changing the size of the calling code.
But there's nothing wrong with it really. On balance, it seems slightly nicer than the poor-man's re-implementation. In the last 5 years, I might have been able to use it maybe twice.
Whereas if you started one promise, waited for it to finish, then started the next and so on, it would take the three seconds as they won't be run in parallel.
The code you've written can be seen as a "poor-man's" Promise.all, in the sense that it's doing roughly the same thing but less clearly. It also behaves slightly differently in terms of rejections: if the final promise in the Promise.all version rejects immediately, then the whole promise will fail immediately. However, in your version, if the final promise rejects, that rejection won't be evaluated by the await (and therefore thrown) until all the other tasks have completed.
For reasons of clarity and correctness, therefore, it's usually better to just use Promise.all rather than awaiting a list of already-started promises in sequence.
But the early rejection is a concrete improvement over the "poor-man's" version. I'm sold.
It is shorter to write than a for of loop, and importantly, all images will be loaded in parallel rather than sequentially, which can be significantly faster.
images = Promise.all(uris.map(loadImage))On the other hand, the language designers are not random framework authors. They know what they're doing. There must be some reason why `Promise.all` exists. I just don't know what it is.
To re-iterate, I understand the difference between serial and parallel tasks. But I have also found that it's possible to do parallel tasks with `await` in a loop. So I'm still missing something.
As others have mentioned; if you aren’t using Promise.all, you are likely missing a good deal of opportunities for easier and more performant async code.
The "inner workings" could be a for loop, with the exception of rejected promises. The only opportunity for quicker resolution is when one of the promises reject, at which time Promise.all() immediately rejects.
I think the allegations of "easier" are significantly overblown too.
// A -- Async and parallel
const results = await Promise.all(data.map((i) => delay(i)));
// B -- Functionally equivalent to the above
const promises = data.map((i) => delay(i));
const result = [];
for (let a of promises) {
const r = await a;
result.push(r);
}
// C -- Problematic and naive approach: much slower
const result = [];
for (let i of data) {
const r = await delay(i);
result.push(r);
}