Recovering from rejected promises in JS

I'm using native promises (mostly) and attempting to recover from an error and continue executing the promise chain.

Effectively, I'm doing this:

  • REST query to see if ID exists. Note that this returns a jquery deferred.
  • .then (success means ID exists, so fail and stop) (fail means ID does not exist, so continue creating ID)
  • .then (create the ID record and send to the server)

I return a Promise.resolve() from my rejected function, which should cause the success part of the next .then to execute. It does not. I've tried this on Chrome and Safari.

Note that the first promise is actually a query deferred, but according to this page (http://api.jquery.com/deferred.then/), deferred.then() returns a promise object. So adding an extra .then should covert to native promises.

To make it clearer - here's the pseudocode:

promise = $.ajax(url);
promise = promise.then();  // convert to promise 
promise.then(function() { cleanup(); return Promise.reject(); },
    function(err) { return Promise.resolve(); });
.then(function() { createIdentityDetails(); });
.then(function() { sendIdentityDetails(); });

Note that I want to FAIL when the ajax returns success, and I want to continue processing when the ajax call fails.

What happens is that the FAIL functions for all subsequent .then portions execute. That is, my return Promise.resolve() doesn't work - which is (I think) in violation of the spec.

I'd appreciate any feedback on how I can deal with and recover from errors in long promise chains.

Many thanks for any advice you can provide.

p.s. creating and collecting the full identity information is quite time consuming, so I don't want to do it if the ID exists. Hence I want to check first and fail quickly.

p.p.s I really like the way that promises have unwound these deeply nested async callback chains.

Answers:

Answer

Assuming createIdentityDetails() and sendIdentityDetails() to be promise-returning asynchronous functions ...

If what we see in the question is the entirety of the promise chain, then handling the error condition is simple. It's not necessary to convert success to failure or failure to success, or from one type of promise to another.

$.ajax(url).then(function() {
    cleanup();
}, function(err) {
    createIdentityDetails()
    .then(sendIdentityDetails);
});

This will work regardless of the type of promise returned by createIdentityDetails() jQuery or non-jQuery.

If, however, there's more to it, eg a caller function needs to be informed of the outcome, then you need to do more, and it depends on how you want the possible outcomes to be reported.

Report 'ID already exists' as failure and 'new ID created' as success

This is what the question suggests

function foo() {
    return $.ajax(url).then(function() {
        cleanup();
        return $.Deferred().reject('failure: ID already exists');
    }, function(err) {
        return createIdentityDetails()
        .then(sendIdentityDetails)
        .then(function() {
            return $.when('success: new ID created');
        });
    });
}

Report both types of outcome as success

This seems more sensible as the handled error will be reported as success. Only unpredicted, unhandled errors will be reported as such.

function foo() {
    return $.ajax(url).then(function() {
        cleanup();
        return 'success: ID already exists';
    }, function(err) {
        return createIdentityDetails()
        .then(sendIdentityDetails)
        .then(function() {
            return $.when('success: new ID created');
        });
    });
}

Whichever reporting strategy is adopted, it matters very much what type of promise createIdentityDetails() returns. As the first promise in the chain it determines the behaviour of both its chained .thens.

  • if createIdentityDetails() returns a native ES6 promise, then no worries, most flavours of promise, even jQuery, will be assimilated.
  • if createIdentityDetails() returns a jQuery promise, then only jQuery promises will be assimilated. Therefore sendIdentityDetails() must also return a jQuery promise (or an ES6 promise which must be recast into jQuery with $.Deferred(...)), as must the final success converter (as coded above).

You can see the effects of mixing jQuery and ES6 promises in these two ways here. The first alert is generated by the second block of code, and is not what is expected. The second alert is generated by the first block and correctly gives the result 98 + 1 + 1 = 100.

Answer
promise = promise.then();  // convert to promise

Huh? A promise returned by $.ajax is already a promise.

promise.then(function() { cleanup(); return Promise.reject(); },
 function(err) { return Promise.resolve(); });

The problem with this is that jQuery is not Promises/A+ compatible, and fails to adopt promises/thenable from other implementations than its own. You would have to use $.Deferred here to make this work, like

promise.then(function() { cleanup(); return $.Deferred().reject(); },
             function() { return $.when(); }); // or return $.Deferred().resolve();

That is, my return Promise.resolve() doesn't work - which is (I think) in violation of the spec.

Indeed it is. However, jQuery is known for this, and they won't fix it until v3.0.

To get the native Promise library you want to use working, you will need to avoid jQuery's then. This can easily be done:

var $promise = $.ajax(url);
var promise = Promise.resolve($promise);  // convert to proper promise 
promise.then(function() {
    cleanup();
    throw undefined;
}, function(err) {
    return undefined;
})
.then(createIdentityDetails)
.then(sendIdentityDetails);
Answer

It seems that JQuery promises do not permit you to change a failure to a success. If, however, you use native promises, you can.

For example:

Promise.resolve()
    .then(function() {console.log("First success"); return Promise.reject(); },
        function() { console.log("First fail"); return Promise.resolve(); })
    .then(function() {console.log("Second success"); return Promise.reject(); },
        function() { console.log("Second fail"); return Promise.resolve(); })
    .then(function() {console.log("Third success"); return Promise.reject(); },
        function() { console.log("Third fail"); return Promise.resolve(); })

Here I return a reject from the first success handler. In the second failure handler I return a resolve. This all works as expected. The output is (Chrome):

First success
Second fail
Third success

It turns out the proper way to deal with jQuery deferreds and promises is to cast them:

var jsPromise = Promise.resolve($.ajax('/whatever.json'));

(from http://www.html5rocks.com/en/tutorials/es6/promises/). This works nicely, so if you change the initial line above to:

Promise.resolve($.ajax("this will fail"))
...

you correctly get:

First fail
Second success
Third fail

Bottom line... cast deferred to promise asap, then everything seems to work right.

Answer

Hopefully this will clear things up a bit, you had a couple of stray ; and you're doing things you don't really need to do in the then functions

firstly, I'm sure you DO NOT want the

promise = promise.then();

line, the code would look like this

promise = $.ajax(url);
promise.then(function() { 
    cleanup(); 
    throw 'success is an error'; // this is equivalent to return Promise.reject('success is an error');
}, function(err) { 
    return 'failure is good';  // returning here means you've nullified the rejection
}) // remove the ; you had on this line
.then(function() { createIdentityDetails(); }) // remove the ; on this line
.then(function() { sendIdentityDetails(); }) // remove the ; on this line
.catch(function(err) { }); // you want to catch the error thrown by success

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us Javascript

©2020 All rights reserved.