6

I have a number of async tasks that need to be completed, so I'm using promises.

I need to detect when each one of the promises has been executed (both resolved and rejected). I must not continue execution until that point.

I was using something like this:

$.when(promise1, promise2, ...).always();

But this code is wrong, because the when method has lazy evaluation, and it returns as soon as one of the promises fails. So the always callback also runs as soon as one of the promises fail.

I was thinking in coding a workaround, but this use case is so common that maybe somebody has done it already, or maybe there's even a way of doing this using just jQuery (if not, it would be nice to add a Promise.whenNonLazy or a Promise.when(promise1, promise2, ..., false) in the future.

Is this possible?

Mister Smith
  • 27,417
  • 21
  • 110
  • 193
  • Using `.when()` with multiple promises as arguments, means you are interested in the combined fulfillment of the promises. If any one of the promises becomes rejected then the promise returned by `when()` will be rejected. So it's `when()` not `always()` that has the behaviour you observe. Neither `.when()` nor `.always()` is lazy. Both are totally correct. It's not clear what behaviour you want but it seems likely that you shouldn't be using `when()`. – Beetroot-Beetroot Oct 04 '13 at 12:43
  • @Beetroot-Beetroot You are right, I've edited the question. But I think `when` shows lazy behaviour. – Mister Smith Oct 04 '13 at 13:08
  • Why "lazy" and not "energetic"? :-) – Beetroot-Beetroot Oct 04 '13 at 13:25
  • It's a duplicate to http://stackoverflow.com/questions/5824615/jquery-when-callback-for-when-all-deferreds-are-no-long-unresolved-either-r The behavior is fully explained there. – lagivan Jun 15 '15 at 21:33

3 Answers3

7

More sophisticated promise libraries have an allSettled() function like Q or Promise.settle like Bluebird.

In jQuery, you could implement such a function yourself as well and extend the $ namespace with it, but that will only be necessary if you need it often and performance-optimized.

A simpler solution would be to create a new promise for each of the ones you are waiting for, and fulfilling them even when the underlying one is rejected. Then you can use $.when() on them without problems. In short:

// using Underscore's .invoke() method:
$.when.apply(null, _.invoke(promises, "then", null, $.when)).done(…)

More stable:

$.when.apply($, $.map(promises, function(p) {
    return p.then(null, function() {
        return $.Deferred().resolveWith(this, arguments);
    });
})).then(…);

You might change the then callbacks a bit to distinguish between fulfilled and rejected results in the final done.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • I think I get the idea, but `$.Deferred` is a function, you'll probably need `$.Deferred()` instead. Also I think you'll need to pass a success callback to `p.then` that will be the same as the error callback. – Mister Smith Oct 07 '13 at 09:13
  • 1
    Ok, I've just understood the `null` argument in that `then`. It lets the resolved promise to be returned as is. But that `$.Deferred` definitely needs parenthesis. – Mister Smith Oct 07 '13 at 09:19
  • If you want to drop in code for a `$.settle()`, there is one here: https://stackoverflow.com/a/35820459/271351 – cjbarth Jun 01 '17 at 19:06
1

That's an interesting property of always - I hadn't expected that behaviour.

I suppose you could use a master, top-level deferred to monitor the states of the main deferreds, which is resolved only once the main deferreds are all either resolved or rejected. Something like:

//set up master deferred, to observe the states of the sub-deferreds
var master_dfd = new $.Deferred;
master_dfd.done(function() { alert('done'); });

//set up sub-deferreds
var dfds = [new $.Deferred, new $.Deferred, new $.Deferred];
var cb = function() {
    if (dfds.filter(function(dfd) {
        return /resolved|rejected/.test(dfd.state());
    }).length == dfds.length)
        master_dfd.resolve();
};
dfds.forEach(function(dfd) { dfd.always(cb); });

//resolve or reject sub-deferreds. Master deferred resolves only once
//all are resolved or rejected
dfds[0].resolve();
dfds[1].reject();
dfds[2].resolve();

Fiddle: http://jsfiddle.net/Wtxfy/3/

Mitya
  • 33,629
  • 9
  • 60
  • 107
  • Yeah, that is the basic idea. I'm doing a similar thing: I create a wrapper deferred for each deferred, then resolve the wrapper when the contained deferred either resolves or is rejected. Finally apply a regular `$.when` over all the wrappers. I still have to work out how to pass the resolved params back to the main deferred (the `$.when` promise, if resolved, receives the list of individual parameters of each promise). I'd like to do the same with my `whenNonLazy` method. – Mister Smith Oct 04 '13 at 09:49
  • `.always()` does take only one callback, why are you passing two? – Bergi Oct 04 '13 at 12:52
1

Smithy,

First let's assume your promises are in an array.

var promises = [....];

What you appear to want is .when() applied to some transform of these promises, such that any rejected promise is converted to resolved, whilst being transparent to promises that are already resolved.

The required operation can be written very succinctly as follows :

$.when.apply(null, $.map(promises, resolvize)).done(...);
//or, if further filtering by .then() is required ...
$.when.apply(null, $.map(promises, resolvize)).then(...);

where resolvize is the transform mechanism.

So what should resolvize(), look like? Let's exploit the characteristics of .then() to make the distinction beteween a resolved and a rejected promise, and respond accordingly.

function resolvize(promise) {
    //Note: null allows a resolved promise to pass straight through unmolested;
    return promise.then(null, function() {
        return $.Deferred().resolve.apply(null, arguments).promise();
    });
}

untested

With resolvize in some outer scope, it can be made available to be used in a $.when.apply($.map(promises, resolvize)) expression wherever it is needed. This is most likely adequate, without going to the extent of extending jQuery with a new method.

Regardless of how the transform is achieved, you end up with a potential issue; namely knowing for each argument of the .done() callback, whether its corresponding promise was originally resolved or rejected. That's the price you pay for converting rejection to resolution. You may, however, be able to detect the original status from the parameter(s) with which the original promises were resolved/rejected.

Beetroot-Beetroot
  • 18,022
  • 3
  • 37
  • 44