470

Here's an contrived example of what's going on: http://jsfiddle.net/adamjford/YNGcm/20/

HTML:

<a href="#">Click me!</a>
<div></div>

JavaScript:

function getSomeDeferredStuff() {
    var deferreds = [];

    var i = 1;
    for (i = 1; i <= 10; i++) {
        var count = i;

        deferreds.push(
        $.post('/echo/html/', {
            html: "<p>Task #" + count + " complete.",
            delay: count
        }).success(function(data) {
            $("div").append(data);
        }));
    }

    return deferreds;
}

$(function() {
    $("a").click(function() {
        var deferreds = getSomeDeferredStuff();

        $.when(deferreds).done(function() {
            $("div").append("<p>All done!</p>");
        });
    });
});

I want "All done!" to appear after all of the deferred tasks have completed, but $.when() doesn't appear to know how to handle an array of Deferred objects. "All done!" is happening first because the array is not a Deferred object, so jQuery goes ahead and assumes it's just done.

I know one could pass the objects into the function like $.when(deferred1, deferred2, ..., deferredX) but it's unknown how many Deferred objects there will be at execution in the actual problem I'm trying to solve.

Alnitak
  • 334,560
  • 70
  • 407
  • 495
adamjford
  • 7,478
  • 6
  • 29
  • 41
  • 2
    related: [Waiting for multiple deferred objects to complete](http://stackoverflow.com/q/14574367/1048572) – Bergi Apr 23 '15 at 14:59
  • Added a new, simpler, answer for this very old question below. You do *not* need to use an array or `$.when.apply` at all to get the same result. – iCollect.it Ltd May 08 '15 at 19:15
  • rolled back question subject, as it was too specific (this isn't just an AJAX problem) – Alnitak Oct 12 '15 at 10:21

9 Answers9

765

To pass an array of values to any function that normally expects them to be separate parameters, use Function.prototype.apply, so in this case you need:

$.when.apply($, my_array).then( ___ );

See http://jsfiddle.net/YNGcm/21/

In ES6, you can use the ... spread operator instead:

$.when(...my_array).then( ___ );

In either case, since it's unlikely that you'll known in advance how many formal parameters the .then handler will require, that handler would need to process the arguments array in order to retrieve the result of each promise.

Alnitak
  • 334,560
  • 70
  • 407
  • 495
  • 4
    This works, awesome. :) I'm amazed I wasn't able to dredge up such a simple change via Google! – adamjford Apr 11 '11 at 20:42
  • 9
    that's because it's a generic method, not specific to `$.when` - `f.apply(ctx, my_array)` will call `f` with `this == ctx` and the arguments set to the _contents_ of `my_array`. – Alnitak Apr 11 '11 at 20:44
  • 4
    @Alnitak: I'm a little embarrassed that I didn't know about that method, considering how long I've been writing JavaScript now! – adamjford Apr 11 '11 at 20:49
  • 5
    FWIW, the link in Eli's answer to an earler question with discussion of passing `$` vs `null` as the first parameter is worth a read. In this particular case it doesn't matter, though. – Alnitak Apr 11 '11 at 20:50
  • sweet, took a bit of searching to find this, but it solved my problem succinctly. – Stephen Jun 05 '11 at 12:16
  • @Stephen: Glad I'm not the only one who didn't know about this. ;) – adamjford Jun 06 '11 at 14:34
  • Actually the other answer to this question (by @Eli) is more correct as it assigns `$` to `this`. – Tomasz Zieliński Dec 17 '12 at 19:28
  • @TomaszZielinski it doesn't make any difference (i.e. it's irrelevant). The `$.when` method makes no use of `this` so you could pass anything you want as the first parameter to `.apply()`. – Alnitak Dec 17 '12 at 22:16
  • 4
    @Alnitak: Yes, but `$` is less typing than `null` and you're safe when `$.when` implementation changes (not that it's likely in this case but why not keep `this` unchanged by default). – Tomasz Zieliński Dec 18 '12 at 13:07
  • The fiddle explains the way to do this. It seems like there should be a shorthand for this, but this works fine. – Jeff Davis Sep 01 '14 at 16:36
  • I like `$.when.apply(this, my_array)` because it simultaneously helps and confuses – Simon_Weaver May 21 '15 at 00:11
  • What if one of the calls in the loop fails? Then done() is not called. How to catch when all are done, whether succeded or failed? For example if you wish to show which succeeded and which failed. See http://jsfiddle.net/YNGcm/1346/ where #3 fails. – marlar Jan 13 '17 at 18:39
  • @marlar that's a whole [other question](http://stackoverflow.com/questions/5824615/jquery-when-callback-for-when-all-deferreds-are-no-longer-unresolved-either/) – Alnitak Jan 14 '17 at 00:19
  • @Alnitak. I realize that now. At first it seemed like a trivial modification of the code example. However, I found a good answer here: http://stackoverflow.com/a/7881733/234466 – marlar Jan 14 '17 at 10:59
  • @TomaszZielinski: in this case it shouldn't be `$.when.apply($.when, my_array)`? – Marco Sulla Apr 02 '19 at 09:01
  • How can I access the response data and status code of each deferred? – Johannes Pertl Jan 04 '21 at 18:44
112

The workarounds above (thanks!) don't properly address the problem of getting back the objects provided to the deferred's resolve() method because jQuery calls the done() and fail() callbacks with individual parameters, not an array. That means we have to use the arguments pseudo-array to get all the resolved/rejected objects returned by the array of deferreds, which is ugly:

$.when.apply($,deferreds).then(function() {
     var objects = arguments; // The array of resolved objects as a pseudo-array
     ...
};

Since we passed in an array of deferreds, it would be nice to get back an array of results. It would also be nice to get back an actual array instead of a pseudo-array so we can use methods like Array.sort().

Here is a solution inspired by when.js's when.all() method that addresses these problems:

// Put somewhere in your scripting environment
if (typeof jQuery.when.all === 'undefined') {
    jQuery.when.all = function (deferreds) {
        return $.Deferred(function (def) {
            $.when.apply(jQuery, deferreds).then(
            // the calling function will receive an array of length N, where N is the number of
            // deferred objects passed to when.all that succeeded. each element in that array will
            // itself be an array of 3 objects, corresponding to the arguments passed to jqXHR.done:
            // ( data, textStatus, jqXHR )
            function () {
                var arrayThis, arrayArguments;

                if (Array.isArray(this)) {
                    arrayThis = this;
                    arrayArguments = arguments;
                }
                else {
                    arrayThis = [this];
                    arrayArguments = [arguments];
                }

                def.resolveWith(arrayThis, [Array.prototype.slice.call(arrayArguments)]);
            },
            // the calling function will receive an array of length N, where N is the number of
            // deferred objects passed to when.all that failed. each element in that array will
            // itself be an array of 3 objects, corresponding to the arguments passed to jqXHR.fail:
            // ( jqXHR, textStatus, errorThrown )
            function () {
                var arrayThis, arrayArguments;

                if (Array.isArray(this)) {
                    arrayThis = this;
                    arrayArguments = arguments;
                }
                else {
                    arrayThis = [this];
                    arrayArguments = [arguments];
                }

                def.rejectWith(arrayThis, [Array.prototype.slice.call(arrayArguments)]);
            });
        });
    }
}

Now you can simply pass in an array of deferreds/promises and get back an array of resolved/rejected objects in your callback, like so:

$.when.all(deferreds).then(function(objects) {
    console.log("Resolved objects:", objects);
});
Ian Kemp
  • 28,293
  • 19
  • 112
  • 138
crispyduck
  • 1,742
  • 1
  • 12
  • 6
  • 1
    There is just a small problem with your code, when there is only one element in the array the results array returns just that result, instead of a array with a single element (which will break the code that expects an array). To fix it, use this function `var toArray = function (args) { return deferreds.length > 1 ? $.makeArray(args) : [args]; }` instead of `Array.prototype.slice.call`. – Luan Nico Jan 14 '16 at 11:47
39

You can apply the when method to your array:

var arr = [ /* Deferred objects */ ];

$.when.apply($, arr);

How do you work with an array of jQuery Deferreds?

Community
  • 1
  • 1
Eli
  • 17,397
  • 4
  • 36
  • 49
  • I actually saw that question but I guess all the extra details in that question caused the answer to my problem (which was right in there) to fly right over my head. – adamjford Apr 11 '11 at 20:50
  • 1
    @adamjford, if it makes you feel any better, I found your question easier to consume (and first on my particular Google search for this exact issue). – patridge Nov 10 '11 at 21:09
  • @patridge: Happy to hear it helped you out! – adamjford Nov 10 '11 at 22:35
  • This is a great answer, but it was unclear to me how this applied to the example in the original question. After consulting the linked question, it became clear that the line "$.when(deferreds).done(function() {" should simply be changed to "$.when.apply($,deferreds).done(function() {". Right? – Garland Pope Aug 11 '18 at 18:18
9

When calling multiple parallel AJAX calls, you have two options for handling the respective responses.

  1. Use Synchronous AJAX call/ one after another/ not recommended
  2. Use Promises' array and $.when which accepts promises and its callback .done gets called when all the promises are return successfully with respective responses.

Example

function ajaxRequest(capitalCity) {
   return $.ajax({
        url: 'https://restcountries.eu/rest/v1/capital/'+capitalCity,
        success: function(response) {
        },
        error: function(response) {
          console.log("Error")
        }
    });
}
$(function(){
   var capitalCities = ['Delhi', 'Beijing', 'Washington', 'Tokyo', 'London'];
   $('#capitals').text(capitalCities);

   function getCountryCapitals(){ //do multiple parallel ajax requests
      var promises = [];   
      for(var i=0,l=capitalCities.length; i<l; i++){
            var promise = ajaxRequest(capitalCities[i]);
            promises.push(promise);
      }
  
      $.when.apply($, promises)
        .done(fillCountryCapitals);
   }
  
   function fillCountryCapitals(){
        var countries = [];
        var responses = arguments;
        for(i in responses){
            console.dir(responses[i]);
            countries.push(responses[i][0][0].nativeName)
        }  
        $('#countries').text(countries);
   }
  
   getCountryCapitals()
})
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div>
  <h4>Capital Cities : </h4> <span id="capitals"></span>
  <h4>Respective Country's Native Names : </h4> <span id="countries"></span>
</div>
vinayakj
  • 5,591
  • 3
  • 28
  • 48
  • 2
    your answer overreaches, and so did your edit to the question title. The OP already knew how to make the AJAX calls and get an array of deferred objects. The _sole_ point of the question was how to pass that array to `$.when`. – Alnitak Oct 12 '15 at 10:25
  • 6
    I thought explaining in detail with example would be better, with available options.and for that I dont think downvote was necessary. – vinayakj Oct 13 '15 at 19:07
  • 2
    the downvote was for 1. even suggesting sync (albeit with a recommendation not to) 2. the poor quality code in the examples (including `for ... in` on an array?!) – Alnitak Oct 13 '15 at 19:39
  • 1
    1. Agreed, should have had `(not recommended)` 2.Not agree - `for ... in` is ok because the array contains only those properties that need (no extra properties). thanx anyways – vinayakj Oct 13 '15 at 19:45
  • 1
    re: 2 - the problem is it might get copied by other people who can't make that guarantee, or have been dumb enough to add to `Array.prototype`. In any event, for non-performance-critical code it would be better to use `.map` instead of a `for` / `push` loop, e.g. `var promises = capitalCities.map(ajaxRequest); $.when.apply($, promises).then(fillCountryCapitals)` - job done. – Alnitak Oct 13 '15 at 19:56
  • Have a hard time finding a way for this. This worked like a charm and simple. Thanks – Gi1ber7 Jun 13 '17 at 20:02
  • @Gi1ber7 nice to know it helped you. – vinayakj Jun 14 '17 at 14:56
5

As a simple alternative, that does not require $.when.apply or an array, you can use the following pattern to generate a single promise for multiple parallel promises:

promise = $.when(promise, anotherPromise);

e.g.

function GetSomeDeferredStuff() {
    // Start with an empty resolved promise (or undefined does the same!)
    var promise;
    var i = 1;
    for (i = 1; i <= 5; i++) {
        var count = i;

        promise = $.when(promise,
        $.ajax({
            type: "POST",
            url: '/echo/html/',
            data: {
                html: "<p>Task #" + count + " complete.",
                delay: count / 2
            },
            success: function (data) {
                $("div").append(data);
            }
        }));
    }
    return promise;
}

$(function () {
    $("a").click(function () {
        var promise = GetSomeDeferredStuff();
        promise.then(function () {
            $("div").append("<p>All done!</p>");
        });
    });
});

Notes:

  • I figured this one out after seeing someone chain promises sequentially, using promise = promise.then(newpromise)
  • The downside is it creates extra promise objects behind the scenes and any parameters passed at the end are not very useful (as they are nested inside additional objects). For what you want though it is short and simple.
  • The upside is it requires no array or array management.
iCollect.it Ltd
  • 92,391
  • 25
  • 181
  • 202
  • 2
    Correct me if I'm wrong, but your approach is effectively nesting $.when( $.when( $.when(...) ) ) so you end up recursively nested 10 levels deep if there's 10 iterations. This doesn't seem very parallel as you have to wait for each level to return a child's nested promise before it can return its own promise - I think the array approach in the accepted answer is much cleaner as it uses the flexible parameter behavior built into the $.when() method. – Anthony McLin Jul 09 '15 at 03:46
  • @AnthonyMcLin: this is intended to provide a simpler alternative to the coding, not better performance (which is negligible with most Async coding), as is done with chaining `then()` calls in a similar way. The behaviour with `$.when` is to act as it it is parallel (not chained). Please try it before throwing away a useful alternative as it does work :) – iCollect.it Ltd Jul 09 '15 at 09:10
  • In any case, processing the result of this promise (which is like a cons-list, nested tuple-arrays) is much worse. – Bergi Aug 05 '15 at 04:51
  • @Bergi: The result is seldom used in parallel cases and certainly not used in this specific question, so the return value problems can be ignored. I have updated with caveats on the use of any returned values. – iCollect.it Ltd Sep 01 '15 at 14:46
  • because compared to the trivial use of `$.when.apply` this causes load of caveats and solves nothing. – Alnitak Oct 13 '15 at 19:37
  • 2
    @Alnitak: Horses for courses. You are certainly entitled to an opinion, but you have obviously not used this yourself. My own opinion is based on practical uses of this technique. It *works* and has uses, so why throw out a tool from the toolbox based on exaggerations like "loads of caveats" (one) and "solves nothing" (not true - it eliminates the array processing and simplifies chaining of parallel promises where the return values are not need, which as you should know are seldom used in parallel processing cases anyway). Downvotes are supposed to be for "this answer is not useful" :) – iCollect.it Ltd Oct 14 '15 at 09:02
  • *Down-voting without comment is just poop without the dog*. The down arrow is for "this answer is not useful". As this is a useful technique, whether you can comprehend it or not, down-voting is self evidently not the appropriate response here :P – iCollect.it Ltd Jun 08 '16 at 13:49
  • 1
    Hi @GoneCoding. May I ask that you do not add voting commentary to your answers? That is suitable for comments, but otherwise it is noise that distracts from the otherwise good content. Thanks. – halfer Jun 12 '16 at 10:04
  • Hmm, well you're in +36 credit so I think you might be worrying too much about it. Once I reached 10K of rep (mod tools) I didn't care about unexplained DVs any more `:-)`. – halfer Jun 12 '16 at 10:15
  • 1
    @halfer: I don't post any more but I am annoyed at the ignorance displayed to anything original. Keeping all new ideas to myself nowadays :) – iCollect.it Ltd Jun 12 '16 at 10:19
5

I want to propose other one with using $.each:

  1. We may to declare ajax function like:

    function ajaxFn(someData) {
        this.someData = someData;
        var that = this;
        return function () {
            var promise = $.Deferred();
            $.ajax({
                method: "POST",
                url: "url",
                data: that.someData,
                success: function(data) {
                    promise.resolve(data);
                },
                error: function(data) {
                    promise.reject(data);
                }
            })
            return promise;
        }
    }
    
  2. Part of code where we creating array of functions with ajax to send:

    var arrayOfFn = [];
    for (var i = 0; i < someDataArray.length; i++) {
        var ajaxFnForArray = new ajaxFn(someDataArray[i]);
        arrayOfFn.push(ajaxFnForArray);
    }
    
  3. And calling functions with sending ajax:

    $.when(
        $.each(arrayOfFn, function(index, value) {
            value.call()
        })
    ).then(function() {
            alert("Cheer!");
        }
    )
    
Pang
  • 9,564
  • 146
  • 81
  • 122
1

If you're transpiling and have access to ES6, you can use spread syntax which specifically applies each iterable item of an object as a discrete argument, just the way $.when() needs it.

$.when(...deferreds).done(() => {
    // do stuff
});

MDN Link - Spread Syntax

relic
  • 1,662
  • 1
  • 16
  • 24
0

I had a case very similar where I was posting in an each loop and then setting the html markup in some fields from numbers received from the ajax. I then needed to do a sum of the (now-updated) values of these fields and place in a total field.

Thus the problem was that I was trying to do a sum on all of the numbers but no data had arrived back yet from the async ajax calls. I needed to complete this functionality in a few functions to be able to reuse the code. My outer function awaits the data before I then go and do some stuff with the fully updated DOM.

    // 1st
    function Outer() {
        var deferreds = GetAllData();

        $.when.apply($, deferreds).done(function () {
            // now you can do whatever you want with the updated page
        });
    }

    // 2nd
    function GetAllData() {
        var deferreds = [];
        $('.calculatedField').each(function (data) {
            deferreds.push(GetIndividualData($(this)));
        });
        return deferreds;
    }

    // 3rd
    function GetIndividualData(item) {
        var def = new $.Deferred();
        $.post('@Url.Action("GetData")', function (data) {
            item.html(data.valueFromAjax);
            def.resolve(data);
        });
        return def;
    }
Cameron Forward
  • 702
  • 8
  • 16
-1

If you're using angularJS or some variant of the Q promise library, then you have a .all() method that solves this exact problem.

var savePromises = [];
angular.forEach(models, function(model){
  savePromises.push(
    model.saveToServer()
  )
});

$q.all(savePromises).then(
  function success(results){...},
  function failed(results){...}
);

see the full API:

https://github.com/kriskowal/q/wiki/API-Reference#promiseall

https://docs.angularjs.org/api/ng/service/$q

Pang
  • 9,564
  • 146
  • 81
  • 122
mastaBlasta
  • 5,700
  • 1
  • 24
  • 26
  • 5
    This is completely irrelevant. – Benjamin Gruenbaum Apr 22 '15 at 17:21
  • @BenjaminGruenbaum How so? All javascript promise libraries share a similar API, and there's nothing wrong with showing the different implementations. I reached this page looking for an answer for angular, and i suspect many other users will reach this page and not necessarily be in a jquery only environment. – mastaBlasta Apr 27 '15 at 14:04
  • 2
    Namely, because jQuery's promises _do not_ share this API, this is completely inappropriate as an answer on Stack Overflow - there are similar answers for Angular and you can ask there. (Not to mention, you should `.map` here but oh well). – Benjamin Gruenbaum Apr 27 '15 at 14:05