How I fixed a bug related to Promise.all and why you should care
Ok, let's go straight to the point. At javascript world Promise.all
is a way to call multiple promises in parallel and wait all to be fullfilled. Look this snippet, putData
is a pseudo code which emulates a slow (very slow) operation, where each one increments 1 second. At this way, this operation is going to take about 3 seconds to complete, once all promises are called together, and Promise.all
takes care to join them and wait for their results. Ok, no bugs in this snippet, no way to break it.
But, this aren't perfect, and Promises will reject, and when this happens, Promise.all
won't wait all to be fullfilled, but throw the exception immediately. Look that:
In this case, all Promises will be called, but once the second one rejects, Promise.all
with throw an exception immediatly, but Promise#1 and Promise#3 were already called and it doesn'ts means they are going to be cancelled. No, they will keep running, but the result won't be returned. Remember, Promise.all
have throwed.
This is the point where a bug can born, a race condition is happening. Think about a scenario where we are creating an account, using multiple promises, and we intend to clear all database, if one part of this process fails. See the code below:
Now, we have two scenarios.
#1: All promises wil be resolved and account is going to be created successfully.
#2: One Promise can reject, and we want to clear database to not have inconsistent persisted data.
Let's suppose that Promise#2 have rejected. So, Promise#1 and Promise#3 were called, and Promise#3 will take 3 seconds to complete, but Promise#2 throwed, and immediately have called clearAll()
. What will happens? Think a bit before continue…clearAll()
was eventually called before Promise#1 and Promise#3 completes, so database will be cleared, but inconsitent data will remains there, once, they are going to be inserted after clearAll()
have been called.
How to solve? Promise.allSettled
to the rescue. This method, is almost same of Promise.all
except it waits for all promises to be resolved, or rejected, and their results are returned as:
{
status: "fulfilled" | "rejected";
reason?: Error;
value: any;
}
Now, let's rewrite the last logic, with this method, and a little helper to convert theirs results to have same signature of Promise.all
:
Using this method, we can ensure that clearAll()
will be always called even if one Promise have rejected, and not changing signatures and results (because the result is wrapped by handleSettledPromiseValue()
).
This little change can avoid big bugs. Don't forget the unit tests.
Merry Christmas everyone!