Break promise chain and call a function based on the step in the chain where it is broken (rejected)

The reason your code doesn’t work as expected is that it’s actually doing something different from what you think it does.

Let’s say you have something like the following:

stepOne()
.then(stepTwo, handleErrorOne)
.then(stepThree, handleErrorTwo)
.then(null, handleErrorThree);

To better understand what’s happening, let’s pretend this is synchronous code with try/catch blocks:

try {
    try {
        try {
            var a = stepOne();
        } catch(e1) {
            a = handleErrorOne(e1);
        }
        var b = stepTwo(a);
    } catch(e2) {
        b = handleErrorTwo(e2);
    }
    var c = stepThree(b);
} catch(e3) {
    c = handleErrorThree(e3);
}

The onRejected handler (the second argument of then) is essentially an error correction mechanism (like a catch block). If an error is thrown in handleErrorOne, it will be caught by the next catch block (catch(e2)), and so on.

This is obviously not what you intended.

Let’s say we want the entire resolution chain to fail no matter what goes wrong:

stepOne()
.then(function(a) {
    return stepTwo(a).then(null, handleErrorTwo);
}, handleErrorOne)
.then(function(b) {
    return stepThree(b).then(null, handleErrorThree);
});

Note: We can leave the handleErrorOne where it is, because it will only be invoked if stepOne rejects (it’s the first function in the chain, so we know that if the chain is rejected at this point, it can only be because of that function’s promise).

The important change is that the error handlers for the other functions are not part of the main promise chain. Instead, each step has its own “sub-chain” with an onRejected that is only called if the step was rejected (but can not be reached by the main chain directly).

The reason this works is that both onFulfilled and onRejected are optional arguments to the then method. If a promise is fulfilled (i.e. resolved) and the next then in the chain doesn’t have an onFulfilled handler, the chain will continue until there is one with such a handler.

This means the following two lines are equivalent:

stepOne().then(stepTwo, handleErrorOne)
stepOne().then(null, handleErrorOne).then(stepTwo)

But the following line is not equivalent to the two above:

stepOne().then(stepTwo).then(null, handleErrorOne)

Angular’s promise library $q is based on kriskowal’s Q library (which has a richer API, but contains everything you can find in $q). Q’s API docs on GitHub could prove useful. Q implements the Promises/A+ spec, which goes into detail on how then and the promise resolution behaviour works exactly.

EDIT:

Also keep in mind that if you want to break out of the chain in your error handler, it needs to return a rejected promise or throw an Error (which will be caught and wrapped in a rejected promise automatically). If you don’t return a promise, then wraps the return value in a resolve promise for you.

This means that if you don’t return anything, you are effectively returning a resolved promise for the value undefined.

Leave a Comment