13

I a promise in such fashion,

function getMode(){
    var deferred = Promise.defer();

    checkIf('A')
    .then(function(bool){
        if(bool){
            deferred.resolve('A');
        }else{
            return checkIf('B');
        }
    }).then(function(bool){
        if(bool){
            deferred.resolve('B');
        }else{
            return checkIf('C');
        }
    }).then(function(bool){
        if(bool){
            deferred.resolve('C');
        }else{
            deferred.reject();
        }
    });

    return deferred.promise;
}

checkIf returns a promise, and yes checkIf cannot be modified.

How do I break out of the chain at the first match? (any way other than explicitly throwing error?)

mido
  • 24,198
  • 15
  • 92
  • 117

8 Answers8

14

Any way other than explicitly throwing error?

You may need to throw something, but it does not have to be an error.

Most promise implementations have method catch accepting the first argument as error type (but not all, and not ES6 promise), it would be helpful under this situation:

function BreakSignal() { }

getPromise()
    .then(function () {
        throw new BreakSignal();
    })
    .then(function () {
        // Something to skip.
    })
    .catch(BreakSignal, function () { })
    .then(function () {
        // Continue with other works.
    });

I add the ability to break in the recent implementation of my own promise library. And if you were using ThenFail (as you would probably not), you can write something like this:

getPromise()
    .then(function () {
        Promise.break;
    })
    .then(function () {
        // Something to skip.
    })
    .enclose()
    .then(function () {
        // Continue with other works.
    });
vilicvane
  • 11,625
  • 3
  • 22
  • 27
11

You can use return { then: function() {} };

.then(function(bool){
    if(bool){
        deferred.resolve('A');
        return { then: function() {} }; // end/break the chain
    }else{
        return checkIf('B');
    }
})

The return statement returns a "then-able", only that the then method does nothing. When returned from a function in then(), the then() will try to get the result from the thenable. The then-able's "then" takes a callback but that will never be called in this case. So the "then()" returns, and the callback for the rest of the chain does not happen.

Martin
  • 521
  • 5
  • 8
3

I would just use coroutines/spawns, this leads to much simpler code:

function* getMode(){
    if(yield checkIf('A'))
        return 'A';
    if(yield checkIf('B'))
        return 'B';
    if(yield checkIf('C'))
        return 'C';
    throw undefined; // don't actually throw or reject with non `Error`s in production
}

If you don't have generators then there's always traceur or 6to5.

Benjamin Gruenbaum
  • 270,886
  • 87
  • 504
  • 504
simonzack
  • 19,729
  • 13
  • 73
  • 118
3

I think you don't want a chain here. In a synchronous fashion, you'd have written

function getMode(){
    if (checkIf('A')) {
        return 'A';
    } else {
        if (checkIf('B')) {
            return 'B';
        } else {
            if (checkIf('C')) {
                return 'C';
            } else {
                throw new Error();
            }
        }
    }
}

and this is how it should be translated to promises:

function getMode(){
    checkIf('A').then(function(bool) {
        if (bool)
            return 'A';
        return checkIf('B').then(function(bool) {
            if (bool)
                return 'B';
            return checkIf('C').then(function(bool) {
                if (bool)
                    return 'C';
                throw new Error();
            });
        });
    });
}

There is no if else-flattening in promises.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • 3
    one of the auti-patterns: then includes then – kenberkeley May 14 '16 at 08:59
  • 1
    @KenBerkeley: `then` includes `else` includes `then`. Please suggest how to flatten it? And no, `then` inside a `then` is absolutely not a promise antipattern. You just must not forget to `return` the promise (as always). – Bergi May 14 '16 at 09:00
  • @Bergi, is this not a candidate for your classic `.catch()` chain? – Roamer-1888 Sep 06 '16 at 10:29
  • @Roamer-1888: It could be solved by throwing and `catch`ing as well, but conditional catches are complicated. Nesting is much easier and keeps the spirit of if/else better. – Bergi Sep 06 '16 at 10:33
  • @Bergi, agreed conditional catches would be horrible but a conditional *throw* at each stage, just like your innermost then handler, with unconditional outer catches would be really neat, and amenable to a generalised `array.reduce()` solution. I would post an answer if this wasn't over a year old. – Roamer-1888 Sep 06 '16 at 11:01
  • @Roamer-1888: The problem is that you must make the `catch` conditional, otherwise it swallows all exceptions. See vilicvane's answer which does that with `BreakSignal`. – Bergi Sep 06 '16 at 11:16
  • OK, point taken, but it's a matter of opinion; for me even with a safeguard against unforeseen throws, a catch chain is more comprehensible than nesting. Maybe I just like catch chains. – Roamer-1888 Sep 06 '16 at 12:19
  • @Bergi I read on the comments above that you said: _`then` inside a `then` is absolutely not a promise antipattern_; but I have always read that _never nest then-ables, it's a promise antipattern, just return them to the chain_. So, why you said that first? – robe007 Aug 01 '19 at 05:37
  • 1
    @robe007 I say [never nest thenables in a `new Promise` constructor](https://stackoverflow.com/q/23803743/1048572), and never nest `then` calls **unnecessarily**. It's great when you can simply `return` a promise and chain another `then` on the outside, and you should do it if your code still works when flattened. But that's not always the case, like [when the inner callback wants to close over variables](https://stackoverflow.com/a/28250687/1048572) or when you want to run parts of your asynchronous code conditionally. Promises are so powerful because they still *allow* this monadic style. – Bergi Aug 01 '19 at 19:18
  • @Bergi Ok, thanks for the explanation. I made an approach using `Promise.all` to _avoid_ the _nested then-ables_. Can you please, [check it out](https://jsfiddle.net/reav5oh2/) and tell what you think? – robe007 Aug 05 '19 at 17:47
  • 1
    @robe007 Looks fine for concurrent execution, but the OP of this question wanted something different: the `checkIf()`s should be made sequentially, and `checkIf('B')` should only be called when the `A` one had failed. – Bergi Aug 05 '19 at 18:13
1

You could create a firstSucceeding function that would either return the value of the first succeeded operation or throw a NonSucceedingError.

I've used ES6 promises, but you can adapt the algorithm to support the promise interface of your choice.

function checkIf(val) {
    console.log('checkIf called with', val);
    return new Promise(function (resolve, reject) {
        setTimeout(resolve.bind(null, [val, val === 'B']), 0);
    });
}

var firstSucceeding = (function () {
    
    return function (alternatives, succeeded) {
        var failedPromise = Promise.reject(NoneSucceededError());  
        return (alternatives || []).reduce(function (promise, alternative) {
            return promise.then(function (result) {
                    if (succeeded(result)) return result;
                    else return alternative();
                }, alternative);
        }, failedPromise).then(function (result) {
            if (!succeeded(result)) throw NoneSucceededError();
            return result;
        });
     }
    
    function NoneSucceededError() {
        var error = new Error('None succeeded');
        error.name = 'NoneSucceededError';
        return error;
    }
})();

function getMode() {
    return firstSucceeding([
        checkIf.bind(null, 'A'),
        checkIf.bind(null, 'B'),
        checkIf.bind(null, 'C')
    ], function (result) {
        return result[1] === true;
    });
}

getMode().then(function (result) {
    console.log('res', result);
}, function (err) { console.log('err', err); });
plalx
  • 42,889
  • 6
  • 74
  • 90
  • @downvoter, Would you mind stating what's wrong with the approach? – plalx Mar 02 '15 at 12:37
  • Not downvoter - but it uses the deferred anti-pattern for no reason - it's a bit pointless to pass deferreds around and to use `Promise.defer()` here. – Benjamin Gruenbaum Mar 02 '15 at 20:13
  • @BenjaminGruenbaum Could you expand on this a bit? How would you allow `getMode` to return a promise that will only be resolved once one of the many async alternatives meet the succeeding condition without creating an extra promise? – plalx Mar 02 '15 at 20:39
  • I'd use `then` chaining. – Benjamin Gruenbaum Mar 02 '15 at 21:17
  • @BenjaminGruenbaum Yeah, I had that in mind initially, but was concerned about having to make additional useless function calls until the chain end was reached even if the first operation succeeded. A successful result will have to traverse the entire chain with that design. I guess that's not that of a big deal. Also, I think your implementation needs special care for the last promise's result, no? Anyway, I updated my answer, is that better? – plalx Mar 02 '15 at 22:26
  • @BenjaminGruenbaum Looks much better that way. Thanks for the insights and simplifications. I didn't capitalized `noneSucceededError` initially because I saw it as a factory method, but it's true that in JS, most native constructors are actually factories so it's better named `NoneSucceededError`. – plalx Mar 02 '15 at 22:54
0

i like a lot of the answers posted so far that mitigate what the q readme calls the "pyramid of doom". for the sake of discussion, i'll add the pattern that i plunked out before searching around to see what other people are doing. i wrote a function like

var null_wrap = function (fn) {
  return function () {
    var i;
    for (i = 0; i < arguments.length; i += 1) {
      if (arguments[i] === null) {
        return null;
      }
    }
    return fn.apply(null, arguments);
  };
};

and i did something totally analogous to @vilicvane's answer, except rather than throw new BreakSignal(), i'd written return null, and wrapped all subsequent .then callbacks in null_wrap like

then(null_wrap(function (res) { /* do things */ }))

i think this is a good answer b/c it avoids lots of indentation and b/c the OP specifically asked for a solution that doesn't throw. that said, i may go back and use something more like what @vilicvane did b/c some library's promises might return null to indicate something other than "break the chain", and that could be confusing.

this is more a call for more comments/answers than a "this is definitely the way to do it" answer.

0

Probably coming late the party here, but I recently posted an answer using generators and the co library that would answer this question (see solution 2):

The code would be something like:

const requestHandler = function*() {

        const survey = yield Survey.findOne({
            _id: "bananasId"
        });

        if (survey !== null) {
            console.log("use HTTP PUT instead!");
            return;
        }

        try {
            //saving empty object for demonstration purposes
            yield(new Survey({}).save());
            console.log("Saved Successfully !");
            return;
        }
        catch (error) {
            console.log(`Failed to save with error:  ${error}`);
            return;
        }

    };

    co(requestHandler)
        .then(() => {
            console.log("finished!");
        })
        .catch(console.log);

You would pretty much write synchronous code that would be in reality asynchronous !

Hope it helps!

Community
  • 1
  • 1
Flame_Phoenix
  • 16,489
  • 37
  • 131
  • 266
0

Try to use libs like thisone:

https://www.npmjs.com/package/promise-chain-break

    db.getData()
.then(pb((data) => {
    if (!data.someCheck()) {
        tellSomeone();

        // All other '.then' calls will be skiped
        return pb.BREAK;
    }
}))
.then(pb(() => {
}))
.then(pb(() => {
}))
.catch((error) => {
    console.error(error);
});
Leonid
  • 21
  • 4