4

Say I have 4 functions: runA(), runB(), runC() and runD().

Using ES6 promises, in a completely successful run, these would all be run one after another:

runA()
.then(runB)
.then(runC)
.then(runD)

If runA or runB fail (reject or throw), I would like to call error1() and then completely stop the chain (not call runC or runD ). This makes me think I should add a single .catch() at the very end of the .then promise chain:

runA()
.then(runB)
.then(runC)     //won't get called if runA or runB throws
.then(runD)     //won't get called if runA or runB throws
.catch(error1)

But if runC fails, I would like to call error2() and still stop the chain (not call runD).

runA()
.then(runB)   
.catch(error1)  //moved up to only handle runA and runB
.then(runC)     //but now this gets called after error1() is run
.then(runD)     
.catch(error2)

Now that I have 2 catch calls in the chain, runC will get called after error1 is run since the result of the catch will default to a resolve. Is my only option to have the error1 function create a promise that it always rejects?

jib
  • 40,579
  • 17
  • 100
  • 158
  • What's wrong with using only one `.catch()`? You can do error triage in the catch callback (`if (error1) error1() else if (error2) error2()...`). The `Error` object you throw can have a message and a name (could be the type like you need, 'RunCError' for example). – Shanoor Dec 24 '15 at 07:29
  • of course, you meant `runA() .then(runB) .then(runC) .then(runD)` - ... other than that, if you **really** can not determine what caused the error, you can `throw` at the end of `error1` to skip past `runC` and `runD` - but you'll need to determine in `error2` that the error came from A or B rather than C or D anyway – Jaromanda X Dec 24 '15 at 08:04

3 Answers3

4

No, having error1 create a promise that always rejects, is not your only option.

You can exploit the fact that .then takes two arguments:

.then(onSuccess, onFailure)

When given two arguments, there's an under-appreciated effect that onFailure will not catch failures in onSuccess. This is usually undesirable, except here, where you can use this fact to branch your decision tree:

runA()
.then(runB)
.then(() => runC().then(runD), error1)
.catch(error2)

This does what you want.

  • if runA or runB fail, then error1 is called and chain stops.
  • if runC or runD fail, then error2 is called and chain stops.

You can also write it like this:

runA()
.then(runB)
.then(() => runC()
  .then(runD)
  .catch(error2),
error1)

var log = msg => div.innerHTML += "<br>" + msg;

// Change which one of these four rejects, to see behavior:
var runA = () => Promise.resolve().then(() => log("a"));
var runB = () => Promise.reject().then(() => log("b"));
var runC = () => Promise.resolve().then(() => log("c"));
var runD = () => Promise.resolve().then(() => log("d"));
var error1 = () => log("error1");
var error2 = () => log("error2");

runA()
.then(runB)
.then(() => runC().then(runD), error1)
.catch(error2)
<div id="div"></div>

Try modifying which one fails in this fiddle.

jib
  • 40,579
  • 17
  • 100
  • 158
  • Definitely a useful technique, and the one I tried to jump to at first. My problem with this is that if I want to catch an exception or rejection further up in the chain, the error callback from the 2nd `then` parameter will cause the outer chain to resolve...unless that error callback itself threw another exception or rejected, in which case, it would cause the next catch statement to be hit, neither of which are really what I might want to happen. I've [updated your fiddle](https://jsfiddle.net/m3vwfhzp/4/) here with the example issue. – rwstoneback Feb 13 '16 at 17:20
  • Well it won't work unless you follow my answer obviously. You have to branch the chain like i show in my fiddle, and you're not doing that. – jib Feb 13 '16 at 18:34
  • Take another look. I did branch it, but with another `then` statement following the branch. So your answer only works of the branch is the next to last part of the chain. – rwstoneback Feb 14 '16 at 03:09
  • My solution has the four functions you asked about. If you want to reject an outer chain, with what error should it be rejected? If you just want to stop the chain without success or failure, you could do `.then(new Promise())`, but that is rarely a good idea, even though I'm trying not to judge. – jib Feb 14 '16 at 06:08
1

What's wrong with using only one .catch()? You can do error triage in the catch callback (if (error1) error1() else if (error2) error2()...). The Error object you throw can have a message and a name (could be the type like you need, 'RunCError' for example).

runA()
    .then(runB)
    .then(runC)     // won't get called if runA or runB throws
    .then(runD)     // won't get called if runA or runB throws
    .catch(handleErrors)

function runA() {
    // ...

    if (err) {
        var error = new Error('Something is wrong...');
        error.name = 'RunAError';
        throw error;
    }
}

function runB() {
    // ...

    if (err) {
        var error = new Error('Something is wrong...');
        error.name = 'RunBError';
        throw error;
    }
}

function runC() {
    // ...

    if (err) {
        var error = new Error('Something is wrong...');
        error.name = 'RunCError';
        throw error;
    }
}

function runD() {
    // ...

    if (err) {
        var error = new Error('Something is wrong...');
        error.name = 'RunDError';
        throw error;
    }
}

function handleErrors(err) {
    if (err.name == 'RunAError') {
        handleAError();
    }

    if (err.name == 'RunBError') {
        handleBError();
    }

    // so on...
}
Shanoor
  • 13,344
  • 2
  • 29
  • 40
  • So is it rare that you would want to use more than 1 catch in a promise chain?And if you do include more than 1 `catch`, and you don't want the next `then` to get fired after that catch, the function inside the catch needs to throw or reject again. – rwstoneback Dec 24 '15 at 15:23
  • Yes. It's very much like synchronous code. If you catch an exception, then it's caught, handled, and positive code flow resumes. That's what catch is for. If that's not what you want, then either don't catch it to begin with, or rethrow it. In that case if you nest catches then you'd expect the outer ones to catch it too, right? – jib Feb 14 '16 at 06:21
1

I just stumbled on the same problem and my solution is so far to explicitly invoke a reject in a catch like in this js bin: https://jsbin.com/yaqicikaza/edit?js,console

Code snippet

const promise1 = new Promise( ( resolve, reject ) => reject( 42 ) );

promise1
  .catch( ( err ) => console.log( err ) ) // 42 will be thrown here
  .then( ( res ) => console.log( 'will execute' ) ) // then branch will execute


const promise2 = new Promise( ( resolve, reject ) => reject( 42 ) );

promise2
  .catch( ( err ) => Promise.reject( ) ) // trigger rejection down the line
  .then( ( res ) => console.log( 'will not execute' ) ) // this will be skipped
Alex Moldovan
  • 2,336
  • 1
  • 16
  • 14