10

I am looking for a way to do a callback after two ajax calls completes:

$.when(
    call1(),
    call2()
).always(function() {
    // Here I want to be sure the two calls are done and to get their responses 
);

The catch is that one of the calls might fail. So, in my code the always will invoked without waiting to the other call.

How can I wait for both calls to done (success or failure)?

Naor
  • 23,465
  • 48
  • 152
  • 268
  • 1
    You may want to look into promises http://api.jquery.com/promise/ – BlueBird Feb 26 '13 at 15:41
  • @BlueBird: How? promise required jquery object which I don't have. Can you add an example? – Naor Feb 26 '13 at 15:47
  • 1
    @BlueBird: `$.when` already returns a promise object, i.e. the OP already uses promises. – Felix Kling Feb 26 '13 at 15:48
  • @FelixKling: $.when is not good for me because it doesn't wait for both calls to complete in case one call fails. – Naor Feb 26 '13 at 15:50
  • @Naor: I know, I wanted to explain to BlueBird that his comment is superfluous. Regarding your problem: I think you have to implement your own `$.when`, which only rejects and resolves once all promises are rejected/resolved. Here is how `$.when` is implemented: https://github.com/jquery/jquery/blob/1.9.1/src/deferred.js#L91. – Felix Kling Feb 26 '13 at 15:55

4 Answers4

12

Here is something that should do the trick:

$.whenAllDone = function() {
    var deferreds = [];
    var result = $.Deferred();

    $.each(arguments, function(i, current) {
        var currentDeferred = $.Deferred();
        current.then(function() {
            currentDeferred.resolve(false, arguments);
        }, function() {
            currentDeferred.resolve(true, arguments);
        });
        deferreds.push(currentDeferred);
    });

    $.when.apply($, deferreds).then(function() {
        var failures = [];
        var successes = [];

        $.each(arguments, function(i, args) {
            // If we resolved with `true` as the first parameter
            // we have a failure, a success otherwise
            var target = args[0] ? failures : successes;
            var data = args[1];
            // Push either all arguments or the only one
            target.push(data.length === 1 ? data[0] : args);
        });

        if(failures.length) {
            return result.reject.apply(result, failures);
        }

        return result.resolve.apply(result, successes);
    });

    return result;
}

Check out this Fiddle to see how it works.

Basically it waits for all Deferreds to finish no matter if they fail or not and collects all the results. If we have failures, the returned Deferred will fail with a list of all failures and resolve with all successes otherwise.

Daff
  • 43,734
  • 9
  • 106
  • 120
  • Nope. I think the more common case is that you don't need to wait for all Deferreds to finish if any of them fails. – Daff Feb 26 '13 at 16:56
  • So is it correct to assume that the order of the succes arguments is the same as the order of the added deferreds? argument[0] from dfd1? – DavidVdd Oct 22 '13 at 09:10
  • I wanted to add onto your fiddle a little. I still wanted whenAllDone to be resolved or rejected based on the above rules, but I also wanted a complete report of which promises resolved/rejected and the data it returned. So I created this, hope its useful to someone: http://jsfiddle.net/KnightOfShaddai/n2pu2fsq/4/ – TJ Kirchner Aug 27 '15 at 15:43
  • This doesn't seem to work if there is only one deferred passed in. Anyone have ideas? http://jsfiddle.net/3h6gwe1x/ – Terry Feb 15 '21 at 19:24
1

It isn't pretty, but you could have a global "completed" variable for each ajax call to set when complete. Each call would also check whether both variables were set, and if so, call your always function.

thelr
  • 1,134
  • 11
  • 30
0

You can also nest the calls:

$.when(call1()).always(function(){
    $.when(call2()).always(function(){
        // Here I want to be sure the two calls are done and to get their responses
    });
});

But of course the two calls will become synchronous to each other.

Richard
  • 1,298
  • 6
  • 17
  • 27
0

Daff's answer is good. There is only one problem. When there is only one deferred, things don't work.

The problem was inside jquery's when method.

jquery.when: function( subordinate /* , ..., subordinateN */ ) { ...

It has a line like:

// If resolveValues consist of only a single Deferred, just use that.
deferred = remaining === 1 ? subordinate : jQuery.Deferred(),

And this changes the shape of the arguments, so I had to put it back to the common shape my code expects (i.e. the same shape when multiple deferreds are passed to whenAllDone)

const jqueryWhenUsesSubordinate = deferreds.length == 1;

const deferredArgs = jqueryWhenUsesSubordinate
    ? [[ arguments[ 0 ], arguments[ 1 ] ]]
    : arguments

$.each(deferredArgs, function (i, resolvedArgs) {
    var target = !resolvedArgs[0] ? failures : successes;
    var data = resolvedArgs[1];
    target.push(data.length === 1 ? data[0] : data);
});

Additionally, I changed the function signature to match more closely to Promise.allSettled in that it should take an array parameter of deferred objects, then instead of looping over arguments to set up the deferreds array, you loop over that parameter passed in.

This allows you to programmatically create a variable length of deferreds into an array and pass that into whenAllDone.

Terry
  • 2,148
  • 2
  • 32
  • 53