How to dodge jQuery promises completely when chaining two async jQuery functions?

I've seen many tutorials on the new EMCA promises advocating avoidance of the "promises" in the jQuery library. They usually say that you can dodge them by doing something like this:

Promise.resolve($.getJSON(url, params)); // voila!  the jQuery promise is "gone"!

However, this doesn't really work when I have to chain two async jQuery functions together. How would I chain two getJSON calls (where the second call depends on the first) together without using jQuery's then() or .when()?

Instead, I only want to use Promise.all etc.

I think a similar question problem would be interleaving jquery and EMCA promises?


Solution 1:

You can adopt either of two approaches ...

Convert then combine :

var p1 = Promise.resolve($.getJSON(url_1, params_1)); // voila 1!
var p2 = Promise.resolve($.getJSON(url_2, params_2)); // voila 2!
var p3 = Promise.all([p1, p2]).then(...);

Combine then convert :

var p1 = $.getJSON(url_1, params_1);
var p2 = $.getJSON(url_2, params_2);
var p3 = Promise.resolve($.when(p1, p2)).then(...); // voila 1 and 2!

Straightforwardly, either approach will give you a native ES6 promise, p3, that resolves when both the jQuery promises resolve, or is rejected when either one of the promises fails.

However, you are probably interested in the results of the two getJSON() calls, and jQuery is awkward in this regard. jQuery's jqXHR promises pass multiple parameters to their success and error callbacks, whereas an ES6 promise will accept just one; the rest will be disregarded. Fortunately, it's fairly simple to bundle the multiple params together to make a single object. This has to be done in jQuery prior to conversion to ES6.

The "convert then combine" code expands as follows :

var p1 = Promise.resolve($.getJSON(url_1, params_1).then(
    function(data, textStatus, jqXHR) {
        return { data:data, textStatus:textStatus, jqXHR:jqXHR };
    },
    function(jqXHR, textStatus, errorThrown) {
        return { jqXHR:jqXHR, textStatus:textStatus, errorThrown:errorThrown };
    }
));
var p2 = Promise.resolve($.getJSON(url_2, params_2).then(
    function(data, textStatus, jqXHR) {
        return { data:data, textStatus:textStatus, jqXHR:jqXHR };
    },
    function(jqXHR, textStatus, errorThrown) {
        return { errorThrown:errorThrown, textStatus:textStatus, jqXHR:jqXHR };
    }
));
var p3 = Promise.all([p1, p2]).then(
    function(results) {
        // results[0] will be an object with properties .data, .textStatus, .jqXHR 
        // results[1] will be an object with properties .data, .textStatus, .jqXHR 
    },
    function(rejectVal) {
        // rejectVal will be an object with properties .errorThrown, .textStatus, .jqXHR
    }
);

The "combine then convert" approach is slightly trickier as the combined results appear (in jQuery) as an arguments list, which itself needs to be converted (still in jQuery) to an Array.

var p1 = $.getJSON(url_1, params_1).then(
    function(data, textStatus, jqXHR) { 
        return { data:data, textStatus:textStatus, jqXHR:jqXHR }; 
    },
    function(jqXHR, textStatus, errorThrown) { 
        return { errorThrown:errorThrown, textStatus:textStatus, jqXHR:jqXHR }; 
    }
);
var p2 = $.getJSON(url_2, params_2).then(
    function(data, textStatus, jqXHR) { 
        return { data:data, textStatus:textStatus, jqXHR:jqXHR };
    },
    function(jqXHR, textStatus, errorThrown) { 
        return { errorThrown:errorThrown, textStatus:textStatus, jqXHR:jqXHR }; 
    }
);
var p3 = Promise.resolve($.when(p1, p2).then(function() {
    return [].slice.call(arguments);// <<< convert arguments list to Array
})).then(
    function(results) { 
        // results[0] will be an object with properties .data, .textStatus, .jqXHR
        // results[1] will be an object with properties .data, .textStatus, .jqXHR
    },
    function(rejectVal) { 
        // rejectVal will be an object with properties .errorThrown, .textStatus, .jqXHR
    }
);

DEMO: resolved

DEMO: rejected

Solution 2:

JavaScript promises are interoperable. You can mix them however you want, all proper libraries1 and native promises do accept thenables2 from any implementation anywhere3. If something foreign appears, they just will do Promise.resolve on it.

So usually you would write your code as if they all used the same promise implementation, and it just works.
However, now you want to ensure that all .then method calls are using your favourite implementation; or you want to use a non-standard method or feature? For that, you will have to explicitly cast all promises on which you are directly invoking methods - and nothing else.

Some examples:

Promise.all([$.ajax(…), $.ajax(…)]).then(…); // just works!
$.ajax(…) // a jQuery promise
.then(…)  // so this would be jQuery `then`, which we don't want.
Promise.resolve($.ajax(…)) // explicit cast
.then(function(data) {     // native `then`
    return $.ajax(…);      //   just works!
})                         // returns a native promise still
.catch(…)                  // so we can use its features

1: Yeah, jQuery isn't one of them until version 3.0
2: all jQuery deferreds and promises are such thenables, though
3: Really everywhere you'd expect a promise, in Promise.resolve, then callback return values, Promise.all arguments, …