I misunderstood Promise.all in JavaScript

Just a guy who loves to write code and watch anime.
My misunderstanding of Promise.all
For a long time I thought Promise.all was used to fire and run promises concurrently.
Every time I encountered code that used two awaits, I always proposed using Promise.all instead. Now, this still holds true, but I've finally understood how things actually work.
Let's look at some code and later I'll show an example where we combined Promise.allSettled with Promise.all to enhance the logging.
const firstPromise = await someFunction();
const secondPromise = await someOtherFunction();
Here await makes sure that we wait for the promises to resolve before moving on to the next line. This can be a bit fishy, because await actually is syntatic sugar.
Under the hood, this might be translated to something like this:
someFunction().then((result) => {
someOtherFunction().then((result2) => {
// ...
});
});
Promise.all
const [firstPromise, secondPromise] = await Promise.all([
someFunction(),
someOtherFunction(),
]);
When we call an async function, it returns a promise. someFunction() returns a promise, someOtherFunction() returns a promise. These functions are called. The promise runs. First it goes to the Web API environment and then when done moves into the microtask queue.
Promise.all says "Let me look at all these promises. When all of them are successfully done, I will return them in an array to you."
That's all it does. It helps us wait for multiple promises to resolve.
Calling promises again
const [firstPromise, secondPromise] = await Promise.all([
someFunction(),
someOtherFunction(),
]);
const [firstPromise2, secondPromise2] = await Promise.all([
someFunction(),
someOtherFunction(),
]);
Here, because we call someFunction() and someOtherFunction() in both Promise.all, we would be firing the promises again. To keep it simple: Every time you call an async function, you create a new promise.
someFunction();
someFunction();
someFunction();
Here we call someFunction() three times. We create three promises that run concurrently. Promises by nature run concurrently. They go into the Web API environment and when done move into the microtask queue.
You might find it confusing that we can just call promises if you're so used to await. But to be clear, .then or await is what we use to "wait" for the result of a promise.
Using the same promise
const firstPromise = await someFunction();
const secondPromise = await someOtherFunction();
const [
firstPromiseResultFromSomeFunction,
secondPromiseResultFromSomeOtherFunction,
] = await Promise.all([firstPromise, secondPromise]);
const [
firstPromiseResultFromSomeFunction2,
secondPromiseResultFromSomeOtherFunction2,
] = await Promise.all([firstPromise, secondPromise]);
Believe it or not, this is fine. We use the same promises in both Promise.all calls. Again, all Promise.all does is observe the promises and wait for them to resolve. It doesn't create new promises. To emphasize, this code is fine. There are not issues with it besides a bit of duplication.
Promise.allSettled with Promise.all
This is a pattern I learned from my last startup. We would combine Promise.allSettled with Promise.all. The problem with Promise.all is that as soon as one promise rejects, the entire Promise.all call will reject. This is why we use Promise.allSettled.
Instead of debugging, fixing one promise, just to realize that another promise is the one that's rejecting, we can use Promise.allSettled to get all information we need and guarantee that there are no issues the next time we run the code.
Typically, the code would look something like inside a function:
const operations = [
{ name: "fetchUserInfo", promise: fetchUserInfo() },
{ name: "fetchPreferences", promise: fetchUserPreferences() },
{ name: "calculateMetrics", promise: calculateMetrics() },
];
// results looks like -> [{status: 'fulfilled', value: ...}, {status: 'fulfilled', value: ...}, {status: 'rejected', reason: ...}]
const results = await Promise.allSettled(operations.map((op) => op.promise));
results.forEach((result, index) => {
if (result.status === "rejected") {
// logger -> e.g. datadog, sentry, etc
logger.error({
// operation -> e.g. fetchUserInfo
operation: operations[index].name,
// error -> e.g. Error: Failed to fetch user info
error: result.reason,
// You can include more fields here if you need
// ...
});
}
});
return await Promise.all(operations.map((op) => op.promise));
Some code will be duplicated. You can extract some of it to clean it up.
function handleAllSettledResults({ operations, results }) {
results.forEach((result, index) => {
if (result.status === "rejected") {
logger.error({
operation: operations[index].name,
error: result.reason,
});
}
});
}






