How to handle error properly in Promise chain?

David Song picture David Song · Mar 28, 2017 · Viewed 9.8k times · Source

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?

Answer

jfriend00 picture jfriend00 · Mar 28, 2017

You have a couple options. First, let's see if I can distill down your requirements.

  1. 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.

  2. 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.