4

How can execute a function after a number of ajax requests have all completed regardless of whether they succeeded or error-ed out?

I've been trying to use $.when.apply(this, array) to pass an array of deferred jqXHR objects. However just like the docs say

In the multiple-Deferreds case where one of the Deferreds is rejected, jQuery.when immediately >fires the failCallbacks for its master Deferred. Note that some of the Deferreds may still be >unresolved at that point.

How can leverage jQuery deferred objects to always wait for all the ajax calls to finish?

Maybe I should create my own deferred that will wrap all the other deferreds? If so I'm not quite clear how to set that up.

berg
  • 614
  • 3
  • 9
  • 23
  • 2
    looks like [this is the solution](http://stackoverflow.com/a/5825233/99985) i was looking for. you have to wrap each ajax call in a deferred that gets resolved via the complete function. then pass the whole array into when(). – berg Aug 28 '12 at 06:09

1 Answers1

0

In the spirit of how the Promise specification is likely going for the future with a PromiseInspection object, here's a jQuery add-on function that tells you when all promises are done, whether fulfilled or rejected:

(function() {    
    // pass either multiple promises as separate arguments or an array of promises
    $.settle = function(p1) {
        var args;
        if (Array.isArray(p1)) {
              args = p1;
        } else {
            args = Array.prototype.slice.call(arguments);
        }

        return $.when.apply($, args.map(function(p) {
            // make sure p is a promise (it could be just a value)
            p = wrapInPromise(p);
            // Make sure that the returned promise here is always resolved with a PromiseInspection object, never rejected
            return p.then(function(val) {
                return new PromiseInspection(true, val);
            }, function(reason) {
                // Convert rejected promise into resolved promise by returning a resolved promised
                // One could just return the promiseInspection object directly if jQuery was
                // Promise spec compliant, but jQuery 1.x and 2.x are not so we have to take this extra step
                return wrapInPromise(new PromiseInspection(false, reason));
            });
        })).then(function() {
              // return an array of results which is just more convenient to work with
              // than the separate arguments that $.when() would normally return
            return Array.prototype.slice.call(arguments);
        });
    }

    // utility functions and objects
    function isPromise(p) {
        return p && (typeof p === "object" || typeof p === "function") && typeof p.then === "function";
    }

    function wrapInPromise(p) {
        if (!isPromise(p)) {
            p = $.Deferred().resolve(p);
        }
        return p;
    }

    function PromiseInspection(fulfilled, val) {
        return {
            isFulfilled: function() {
                return fulfilled;
            }, isRejected: function() {
                return !fulfilled;
            }, isPending: function() {
                // PromiseInspection objects created here are never pending
                return false;
            }, value: function() {
                if (!fulfilled) {
                    throw new Error("Can't call .value() on a promise that is not fulfilled");
                }
                return val;
            }, reason: function() {
                if (fulfilled) {
                    throw new Error("Can't call .reason() on a promise that is fulfilled");
                }
                return val;
            }
        };
    }
})();

Then, you can use it like this:

$.settle(promiseArray).then(function(inspectionArray) {
    inspectionArray.forEach(function(pi) {
        if (pi.isFulfilled()) {
            // pi.value() is the value of the fulfilled promise
        } else {
            // pi.reason() is the reason for the rejection
        }
    });
});

Keep in mind that $.settle() will always fulfill (never reject) and the fulfilled value is an array of PromiseInspection objects and you can interrogate each one to see if it was fulfilled or rejected and then fetch the corresponding value or reason. See the demo below for example usage:

Working demo: https://jsfiddle.net/jfriend00/y0gjs31r/

jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • Would `return $.when(p).then(...);` not avoid the need for all that type testing? – Roamer-1888 Mar 06 '16 at 16:24
  • @Roamer-1888 - Probably, but I was specifically trying to avoid wrapping in yet another promise when that isn't needed. So, I'm testing for the case of when it's already a promise and thus does not need to be wrapped again. I could probably put that type testing off in a utility function call `isPromise()` and it would look cleaner. – jfriend00 Mar 06 '16 at 17:40
  • Are you aware that [$.when(p) returns p if p is a promise](https://jsfiddle.net/pdjhhqo3/)? – Roamer-1888 Mar 06 '16 at 17:46
  • You are right, it's a special case but one you can rely on here because the outer `$.when()` would ensure an inner `$.when()` would see only settled promises. – Roamer-1888 Mar 06 '16 at 18:11
  • @Roamer-1888 - I could certainly replace `p = $.Deferred().resolve(p);` with `p = $.when(p);` as a coding shortcut, but I don't understand what else you are suggesting? When the `.map()` function is running, these are usually unsettled promises at that point (unless a settled promise was passed to `$.settle()` which isn't the normal design case). – jfriend00 Mar 06 '16 at 18:26
  • Dang, the. map() executes first! I thought I spotted an efficient way to allow you to avoid the ugly type testing but I think you're stuck with it. – Roamer-1888 Mar 06 '16 at 18:40
  • @Roamer-1888 - You have given me a couple ideas for code cleanup. I'll add those. – jfriend00 Mar 06 '16 at 18:41