How to properly break out of a promise chain?

32,463

Solution 1

Firstly, I think it better to say you are seeking to "bypass" (part of) the promise chain rather than to "break" it.

As you say, testing for "emptyResult" in several places is pretty ugly. Fortunately, a more elegant mechanism is available while adhering to the same general principle of not executing some of the promise chain.

An alternative mechanism is to use promise rejection to control the flow, then to re-detect the specific error condition(s) later in the chain, and put it back on the success path.

Menus.getCantinas().then(function(cantinas) {
    Menus.cantinas = cantinas;
    if(cantinas.length == 0) {
        return $.Deferred().reject(errMessages.noCantinas);
    } else {
        return $.when(Menus.getMeals(cantinas), Menus.getSides(cantinas));
    }
}).then(function(meals, sides) {
    Menus.sides = sides;
    Menus.meals = meals;
    return Menus.getAdditives(meals, sides);
}).then(function(additives) {
    Menus.additives = additives;
    return Menus;
}).then(null, function(err) {
    //This "catch" exists solely to detect the noCantinas condition 
    //and put the chain back on the success path.
    //Any genuine error will be propagated as such.
    //Note: you will probably want a bit of safety here as err may not be passed and may not be a string.
    return (err == errMessages.noCantinas) ? $.when(Menus) : err;
}).done(function(Menus) {
    // with no cantinas, or with everything
});

var errMessages = {
    'noCantinas': 'no cantinas'
};

On the plus side, I find the lack of nesting makes for better readability of the natural success path. Also, for me at least, this pattern would require minimal mental juggling to accommodate further bypasses, if needed.

On the down side, this pattern is slightly less efficient than Bergi's. Whereas the main path has the same number of promises as Bergi's, the cantinas.length == 0 path requires one more (or one per bypass if multiple bypasses were coded). Also, this pattern requires reliable re-detection of specific error condition(s) - hence the errMessages object - which some may find detracts.

Solution 2

Sounds like you want to branch, not to break - you want to continue as usual to the done. A nice property of promises is that they don't only chain, but also can be nested and unnested without restrictions. In your case, you can just put the part of the chain that you want to "break" away inside your if-statement:

Menus.getCantinas().then(function(cantinas) {
    Menus.cantinas = cantinas;

    if (cantinas.length == 0)
        return Menus; // break!

    // else
    return $.when(Menus.getMeals(cantinas), Menus.getSides(cantinas))
    .then(function(meals, sides) {
        Menus.sides = sides;
        Menus.meals = meals;
        return Menus.getAdditives(meals, sides);
    }).then(function(additives) {
        Menus.additives = additives;
        return Menus;
    });
}).done(function(Menus) {
    // with no cantinas, or with everything
});

Solution 3

For folks using built-in browser promises and looking for a way to halt the promise chain without making all consumers know about the rejection case, triggering any chained then's or catches or throwing any Uncaught (in promise) errors, you can use the following:

var noopPromise = {
  then: () => noopPromise, 
  catch: () => noopPromise
}

function haltPromiseChain(promise) {
  promise.catch(noop)

  return noopPromise
}

// Use it thus:
var p = Promise.reject("some error")
p = haltPromiseChain(p)
p.catch(e => console.log(e)) // this never happens

Basically, noopPromise is a basic stubbed out promise interface that takes chaining functions, but never executes any. This relies on the fact that apparently the browser uses duck-typing to determine if something is a promise, so YMMV (I tested this in Chrome 57.0.2987.98), but if that becomes a problem you could probably create an actual promise instance and neuter its then and catch methods.

Share:
32,463
Dennis G
Author by

Dennis G

Click on CTRL+About me on your keyboard and you will be surprised!

Updated on June 27, 2020

Comments

  • Dennis G
    Dennis G about 4 years

    Based on the question here: jQuery chaining and cascading then's and when's and the accepted answer, I want to break the promise chain at a point but haven't yet found the correct way. There are multiple posts about this, but I am still lost.

    Taking the example code from the original question:

    Menus.getCantinas().then(function(cantinas){ // `then` is how we chain promises
        Menus.cantinas = cantinas;
        // if we need to aggregate more than one promise, we `$.when`
        return $.when(Menus.getMeals(cantinas), Menus.getSides(cantinas));
    }).then(function(meals, sides){ // in jQuery `then` can take multiple arguments
        Menus.sides = sides; // we can fill closure arguments here
        Menus.meals = meals;
        return Menus.getAdditives(meals, sides); // again we chain
    }).then(function(additives){
        Menus.additives = additives;
        return Menus; // we can also return non promises and chain on them if we want
    }).done(function(){ // done terminates a chain generally.
         // edit HTML here
    });
    

    How would I break the chain if cantinas.length == 0? I would not want to get the meals, neither the additives, frankly I would want to call some kind of "empty result" callback. I have tried the following which is very ugly (but works...). Teach me the correct way. This still is a valid result, so not a "fail" per se, just empty result I would say.

    var emptyResult = false;
    Menus.getCantinas().then(function(cantinas){
        Menus.cantinas = cantinas;
        if (cantinas.length == 0) {
          emptyResult = true;
          return "emptyResult"; //unuglify me
        }
        return $.when(Menus.getMeals(cantinas), Menus.getSides(cantinas));
    }).then(function(meals, sides){ 
        if (meals == "emptyResult") return meals;  //look at my ugliness...
        Menus.sides = sides;
        Menus.meals = meals;
        return Menus.getAdditives(meals, sides);
    }).then(function(additives){
        if (additives == "emptyResult") return additives;
        Menus.additives = additives;
        return Menus;
    }).done(function(){
       if (emptyResult)
         //do empty result stuff
       else
         // normal stuff
    });
    
  • Dennis G
    Dennis G about 9 years
    Sounds like the right way, but when returning Menus all the following calls are still being made (i.e. .then(function(meals, sides)... and then(function(additives)). These calls should not be made as there are no cantinas (first call empty). When returning Menus, meals == Menus in the following then.
  • Bergi
    Bergi about 9 years
    @DennisG: You need to move the then that takes meals. It should no more be following the getCantinas().then(…), but it should be nested inside the callback. I hoped the indentation would make that clear
  • Dennis G
    Dennis G about 9 years
    Ah! It does now. I overlooked the different nesting of the following then's. Great!
  • Dennis G
    Dennis G about 9 years
    I actually like this pattern better despite the efficiency. I actually believe an error object makes sense, because as you said there might be other cases. And this return $.Deferred().reject(errMessages.noCantinas); is beautiful in my eyes, though it is not a "real" reject, but that's ok.
  • Roamer-1888
    Roamer-1888 about 9 years
    I should really have put a comment against return $.Deferred().reject(errMessages.noCantinas); - something like //bypass-rejection. It's reassuring that you can understand the pattern without that comment.
  • Yerken
    Yerken over 8 years
    this looks like exactly what Promises are made to avoid, callback hell
  • Bergi
    Bergi over 8 years
    @Yerken: I don't see any callback hell here. All of these functions return something useful.
  • Jeff Bowman
    Jeff Bowman almost 8 years
    This is the right solution; it perfectly matches the synchronous version without misusing promise rejections. (You wouldn't break out of an inner loop by throwing an arbitrary error just to catch it outside, right?) The inner promise that the outer then returns can be refactored into its own function, just like any other Javascript refactor.
  • mikermcneil
    mikermcneil over 6 years
    Excellent answer, thanks. It works in node too-- our team relies on this ducktyping behavior in V8 in combination with await for the Waterline project. To emulate early returns, we've started experimenting with a few more complex options, but this is a great lead! I'll look into it tonight. If I find anything problematic (e.g. memory leaks), I'll try and remember to report back, but if you or anyone else is curious and I haven't followed up, feel free to ping me here as a reminder.
  • CaptRespect
    CaptRespect over 5 years
    This is a great solution. Much nicer than faking an error or nesting promise chains. Thanks!