1

Let's suppose I have a Parse Cloud Code js function which I want to return a promise, like:

function doSomething(myObj, abortIfSaveFails) {
  var dsPromise = new Parse.Promise();
  myObj.set("name", "abc");
  myObj.save().then(function(){
    // location "A"
    // great, it worked!
    // don't want to actually do anything more in this block because 
    // we might want to do the same things even if save fails, and I 
    // don't want to repeat code
    return Parse.Promise.as();
  }, function(error){
    // location "B"
    // save failed, but we might want to keep going
    if (abortIfSaveFails) {
      // location "C": wish I could abort the whole promise chain here!
      return Parse.Promise.error();
    } else {
      return Parse.Promise.as();
    }
  }).then(function(){
    // location "D"
    // at this point we're not sure if save succeeded but let's 
    // assume we don't need to know
    return doSomethingCruciallyImportantAndReturnAPromise();
  }, function(error){  
    // location "E": 
    // not sure if we got here because doSomethingCruciallyImportant...() errored or 
    // because myObj.save() errored.
    // would be nice to abort the whole thing right now!
    return Parse.Promise.error();
  }).then(function(){
    // location "F"
    // at this point we know doSomethingElse... succeeded
    return doSomethingUnimportantAndReturnAPromise();
  }, function(error){
    // location "G"
    // not sure if we got here because doSomethingCruciallyImportant...() errored or
    // because doSomethingUnimportant...() errored.
    // If doSomethingCruciallyImportant...() succeeded but doSomethingUnimportant...()
    // failed, I'd LIKE to do dsPromise.resolve()...  but I can't resolve, because 
    // we might be in the process of aborting the function because myObj.save() rejected,
    // or doSomethingCruciallyImportant rejected!
    dsPromise.reject(); // might not be what I want to do!
  }).then(function(){
    // location "H"
    // everything worked fine
    dsPromise.resolve();
  });
  // location "I"
  return dsPromise; // return the promise so `then` will wait
}

How can I refactor/rewrite this to better handle the situations at locations C, E, and G?

I realize I could dsPromise.reject() at C and E, but what would happen to the currently executing promise chain? Wouldn't it keep executing and move on to D, E, F, G etc.? Then couldn't I get to a place where I'm resolving dsPromise multiple times?

Ben Wheeler
  • 6,788
  • 2
  • 45
  • 55
  • Your `dsPromise` is an example of the [deferred antipattern](https://stackoverflow.com/questions/23803743/what-is-the-deferred-antipattern-and-how-do-i-avoid-it). And since you don't reject the promise (but return undefined from the error handler in which `dsPromise.reject()` is called), you probably are already trying to resolve it multiple times. – Bergi Sep 15 '14 at 18:33
  • Hm, I see what you mean! I call reject() but don't tell the promise block whose execution I'm already in what to return. – Ben Wheeler Sep 16 '14 at 15:41

1 Answers1

4

How can I refactor/rewrite this to better handle the situations at locations C, E, and G?

Nest handlers appropriately. If a handler is supposed to handle the resolution of only a single action, then chain it with .then directly on the promise for that action, and not somewhere else in the chain.

In location E, you would not attach a handler to the chain that includes the save call, but only to promises in the onFulfilled (i.e. not aborted) branch.

doSomethingCruciallyImportant().then(function(){
    // location "F"
    // at this point we know doSomethingCruciallyImportant succeeded
    return doSomethingUnimportant();
}, function(error){
    // location "G"
    // not sure if we got here because doSomethingCruciallyImportant() errored
    // or because doSomethingUnimportant() errored.
});

No. Read more about how the onRejection handler of .then() actually works - it is not called when the onFulfilled handler is executed (which calls doSomethingUnimportant() here). In location G, you know for sure that something in the chain before the then call failed - doSomethingCruciallyImportant() in my simplified snippet.

In combination:

function doSomething(myObj, abortIfSaveFails) {
  myObj.set("name", "abc");
  return myObj.save().then(null, // no onFulfilled block at all!
    function(error){
    if (abortIfSaveFails) {
      return Parse.Promise.error(); // abort
    } else {
      return Parse.Promise.as(); // keep going regardless of the save fail
    }
  }).then(function() {
    // either save succeeded or we don't care about it
    return doSomethingCruciallyImportantAndReturnAPromise()
    .then(function(){
       // location "F"
       // at this point we know doSomethingCruciallyImportant succeeded
       return doSomethingUnimportantAndReturnAPromise().then(null, function(err) {
         return Parse.Promise.as(); // we don't care if it errored
       });
    } //, function(error){
      // location "G"
      // doSomethingCruciallyImportant...() errored
      // return Parse.Promise.error();
      // }
    );
  }).then(function(result) {
    // location "H"
    // everything worked fine: Save succeeded (or we didn't care) and 
    // doSomethigCruciallyImportant() did as well
    return result;
  });
}
Community
  • 1
  • 1
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Thanks for the in-depth answer! 2 questions: – Ben Wheeler Sep 16 '14 at 15:31
  • 1. What if I were add an error handler block after H in your example? Then, could it be invoked by both/either of the //abort line, and the return do...Crucially call? Aren't there code situations where it's necessary to figure out why function(error) blocks were called? Or should you always nest promises in those situations? – Ben Wheeler Sep 16 '14 at 15:35
  • 2. If promises require so much nesting, why use promises at all instead of Backbone-style success/error callbacks? – Ben Wheeler Sep 16 '14 at 15:35
  • Thanks for pointing out my misunderstanding of the onRejection handler of .then() ! – Ben Wheeler Sep 16 '14 at 15:35
  • reading this: http://stackoverflow.com/questions/22539815/arent-promises-just-callbacks/22562045#22562045 to help me answer question #2 – Ben Wheeler Sep 16 '14 at 15:44
  • 1. Yes, it would be called both on aborts and when `do…Important` fails. I prefer to nest handlers if I need to distinguish the origin of an error. Promises are often compared to async try/catch blocks, which you usually would nest similarly to deal with such situations. The alternative is to let all exceptions bubble and then distinguish the origins based on the error object. There are [helper methods](https://github.com/petkaantonov/bluebird/blob/master/API.md#catchfunction-errorclassfunction-predicate-function-handler---promise) that help you to deal with such :-) – Bergi Sep 16 '14 at 15:51
  • 2. Glad you found my answer on that topic already. As you can see, promises have much more advantages than just error handling. Of course, with all your explicit error handling the amount of code might be equal. However, even with all the `onRejected` handlers, the guarantees of promises still hold; like implicit propagation of resolutions (whether fulfillments or rejections). They're much less error-prone. – Bergi Sep 16 '14 at 15:55