I misunderstood Promise.all in JavaScript

Just a guy who loves to write code and watch anime.
Search for a command to run...

Just a guy who loves to write code and watch anime.
No comments yet. Be the first to comment.
Qualities that make outstanding builders.

Bipedal vs quadrupedal: how many legs This is about the number of legs the creature walks on. Bipedal. Two legs. Humans, ostriches, T-rex, kangaroos, most fantasy humanoids. Quadrupedal. Four legs. Do

Intro You hear "lerp" and "smoothstep" everywhere in game dev. They sound like math jargon. They're not. Both are small tools that do the same job: smoothly move from one value to another. The problem

What is Kinematics Kinematics is the math field. It's the study of how things move without worrying about forces (which would be dynamics). FK and IK are the two branches: forward kinematics and inver

Intro Textures are usually the biggest cost in a 3D scene. Memory, bandwidth, and load time all get eaten by them. Resizing them is the obvious lever. There's more. This post is about the less obvious

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) => {
// ...
});
});
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.
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.
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.
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,
});
}
});
}