3

jQuery has a nice feature of its Deferred API, $.wait() for working with multiple Deferreds / Promises. It returns when:

  • All of the Deferreds have been resolve()d

or

  • One of the Deferreds has been reject()ed

Most of the time this is what you want, but sometimes you want to know when all of them have been reject()ed.

Is there a simple or elegant way to do something like $.wait() but only when all Deferreds have been rejected?

(There may be other use cases but mine is to combine this with waiting for the first of several Deferred's to resolve.)

Community
  • 1
  • 1
hippietrail
  • 15,848
  • 18
  • 99
  • 158
  • Related but not the same: [jQuery.when - Callback for when ALL Deferreds are no long 'unresolved' (either resolved or rejected)?](http://stackoverflow.com/questions/5824615/jquery-when-callback-for-when-all-deferreds-are-no-long-unresolved-either-r) – hippietrail Aug 16 '12 at 12:23
  • I filed another feature request against jQuery for this and related enhancements to `$.when()`: **[OPTIONS PARAMETER FOR $.WHEN() TO PROVIDE ALTERNATIVE SEMANTICS](http://bugs.jquery.com/ticket/12325)**. It was also closed but [jaubourg](https://github.com/jaubourg) added good analysis and suggestions that will help anyone interested in this question. – hippietrail Aug 17 '12 at 12:26

2 Answers2

3

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:

// 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);
    }

    function PromiseInspection(fulfilled, val) {
        return {
            isFulfilled: function() {
                return fulfilled;
            }, value: function() {
                return fulfilled ? val: undefined;
            }, reason: function() {
                return !fulfilled ? val: undefined;
            }
        };
    }
    return $.when.apply($, args.map(function(p) {
        // if incoming value, not a promise, then wrap it in a promise
        if (!p || (!(typeof p === "object" || typeof p === "function")) || typeof p.then !== "function") {
            p = $.Deferred().resolve(p);
        }
        // Now we know for sure that p is a promise
        // 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
            // this is required in jQuery 1.x and 2.x (works in jQuery 3.x, but the extra .resolve() is not required in 3.x)
            return $.Deferred().resolve(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);
    });
}

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
  • It seems this code can be improved/simplified by returning an `Error(reason)` if the promise rejects, and the value if it resolves, instead of dealing with `PromiseInspection`. Then, in the consumer, you can just test for `result instanceof Error` to find promises that originally rejected and get their rejection reason. – cjbarth Jun 01 '17 at 18:55
  • @cjbarth - Yes, that is another way to do it. Some prefer this way, some that way. Both work fine. At one time, there was some standards discussion around using something like the `PromiseInspection` object for these types of uses, but I don't know whether that's still a likely direction or not - that's why it was done this way. I agree, it might seem a bit simpler to do it the way you suggested. That does require that all possible rejections of all promises you're using reject with an `instanceof Error` which is reasonable, but not always done. – jfriend00 Jun 02 '17 at 21:49
0

i had the same problem lately: I think that occurs because $.when is more of a Promise.all implementation.

I wasn't able to find a Promise.allSettled alternative in the jQuery documentation and across the web so here's my attempt to get it:

const deferredsAllSettled = deferreds => {
    const settlements = [];

    deferreds.forEach(deferred => {
        const settlement = jQuery.Deferred();

        deferred.always(settlement.resolve);

        settlements.push(settlement);
    });

    const returnedDeferred = $.Deferred();

    jQuery.when
        .apply(jQuery, settlements)
        .then(() =>
            returnedDeferred.resolve(
                deferreds.map(deferred => deferred.promise())
            )
        );

    return returnedDeferred.promise();
};

NOTE: I've written that for a utils file so the Promise.allSettled one-only iterable parameter signature was ok for me, if you want to write it in the $.when spirit (passing and getting the $.Deferreds as separate arguments) you'll have to write a little more...

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Daniele Fioroni
  • 180
  • 2
  • 4