Say we have 3 asynchronous tasks that return Promises: A
, B
and C
. We want to chain them together (that is, for sake of clarity, taking the value returned by A
and calling B
with it), but also want to handle the errors correctly for each, and break out at the first failure. Currently, I see 2 ways of doing this:
A .then(passA) .then(B) .then(passB) .then(C) .then(passC) .catch(failAll)
Here, the passX
functions handle each of the success of the call to X
. But in the failAll
function, we'd have to handle all of the errors of A
, B
and C
, which may be complex and not easy to read, especially if we had more than 3 async tasks. So the other way takes this into consideration:
A .then(passA, failA) .then(B) .then(passB, failB) .then(C) .then(passC, failC) .catch(failAll)
Here, we separated out the logic of the original failAll
into failA
, failB
and failC
, which seems simple and readable, since all errors are handled right next to its source. However, this does not do what I want.
Let's see if A
fails (rejected), failA
must not proceed to call B
, therefore must throw an exception or call reject. But both of these gets caught by failB
and failC
, meaning that failB
and failC
needs to know if we had already failed or not, presumably by keeping state (i.e. a variable).
Moreover, it seems that the more async tasks we have, either our failAll
function grows in size (way 1), or more failX
functions gets called (way 2). This brings me to my question:
Is there a better way to do this?
Consideration: Since exceptions in then
is handled by the rejection method, should there be a Promise.throw
method to actually break off the chain?
A possible duplicate, with an answer that adds more scopes inside the handlers. Aren't promises supposed to honor linear chaining of functions, and not passing functions that pass functions that pass functions?
There are two ways in which you can handle errors in your promise chain, either by passing an error handler to then block or using the catch operator.
Promise resolve() method: Any of the three things can happened: If the value is a promise then promise is returned. If the value has a “then” attached to the promise, then the returned promise will follow that “then” to till the final state. The promise fulfilled with its value will be returned.
We must always add a catch() , otherwise promises will silently fail. In this case, if thePromise is rejected, the execution jumps directly to the catch() method. You can add the catch() method in the middle of two then() methods, but you will not be able to break the chain when something bad happens.
A promise is just an object with properties in Javascript. There's no magic to it. So failing to resolve or reject a promise just fails to ever change the state from "pending" to anything else. This doesn't cause any fundamental problem in Javascript because a promise is just a regular Javascript object.
You have a couple options. First, let's see if I can distill down your requirements.
You want to handle the error near where it occurs so you don't have one error handler that has to sort through all the possible different errors to see what to do.
When one promise fails, you want to have the ability to abort the rest of the chain.
One possibility is like this:
A().then(passA).catch(failA).then(val => { return B(val).then(passB).catch(failB); }).then(val => { return C(val).then(passC).catch(failC); }).then(finalVal => { // chain done successfully here }).catch(err => { // some error aborted the chain, may or may not need handling here // as error may have already been handled by earlier catch });
Then, in each failA
, failB
, failC
, you get the specific error for that step. If you want to abort the chain, you rethrow before the function returns. If you want the chain to continue, you just return a normal value.
The above code could also be written like this (with slightly different behavior if passB
or passC
throws or returns a rejected promise.
A().then(passA, failA).then(val => { return B(val).then(passB, failB); }).then(val => { return C(val).then(passC, failC); }).then(finalVal => { // chain done successfully here }).catch(err => { // some error aborted the chain, may or may not need handling here // as error may have already been handled by earlier catch });
Since these are completely repetitive, you could make the whole thing be table-driven for any length of sequence too.
function runSequence(data) { return data.reduce((p, item) => { return p.then(item[0]).then(item[1]).catch(item[2]); }, Promise.resolve()); } let fns = [ [A, passA, failA], [B, passB, failB], [C, passC, failC] ]; runSequence(fns).then(finalVal => { // whole sequence finished }).catch(err => { // sequence aborted with an error });
Another useful point when chaining lots of promises is if you make a unique Error class for each reject error, then you can more easily switch on the type of error using instanceof
in the final .catch()
handler if you need to know there which step caused the aborted chain. Libraries like Bluebird, provide specific .catch()
semantics for making a .catch()
that catches only a particular type of error (like the way try/catch does it). You can see how Bluebird does that here: http://bluebirdjs.com/docs/api/catch.html. If you're going to handle each error right at it's own promise rejection (as in the above examples), then this is not required unless you still need to know at the final .catch()
step which step caused the error.
There are two ways that I recommend (depending on what you are trying to accomplish with this):
Yes, you want to handle all errors in the promise chain with a single catch.
If you need to know which one failed, you can reject the promise with a unique message or value like this:
A .then(a => { if(!pass) return Promise.reject('A failed'); ... }) .then(b => { if(!pass) return Promise.reject('B failed'); ... }) .catch(err => { // handle the error });
Alternatively, you can return other promises inside of .then
A .then(a => { return B; // B is a different promise }) .then(b => { return C; // C is another promise }) .then(c => { // all promises were resolved console.log("Success!") }) .catch(err => { // handle the error handleError(err) });
In each of those promises, you will want some kind of unique error message so you know which one failed.
And since these are arrow functions we can remove the braces! Just another reason I love promises
A .then(a => B) .then(b => C) .then(c => console.log("Success!")) .catch(err => handleError(err));
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With