132

I have an application that requires data be loaded in a certain order: the root URL, then the schemas, then finally initialize the application with the schemas and urls for the various data objects. As the user navigates the application, data objects are loaded, validated against the schema, and displayed. As the user CRUDs the data, the schemas provide first-pass validation.

I'm having a problem with initialization. I use an Ajax call to fetch the root object, $.when(), and then create an array of promises, one for each schema object. That works. I see the fetch in the console.

I then see the fetch for all the schemas, so each $.ajax() call works. fetchschemas() does indeed return an array of promises.

However, that final when() clause never fires and the word "DONE" never appears on the console. The source code to jquery-1.5 seems to imply that "null" is acceptable as an object to pass to $.when.apply(), as when() will build an internal Deferred() object to manage the list if no object is passed in.

This worked using Futures.js. How should an array of jQuery Deferreds be managed, if not like this?

    var fetch_schemas, fetch_root;

    fetch_schemas = function(schema_urls) {
        var fetch_one = function(url) {
            return $.ajax({
                url: url,
                data: {},
                contentType: "application/json; charset=utf-8",
                dataType: "json"
            });
        };

        return $.map(schema_urls, fetch_one);
    };

    fetch_root = function() {
        return $.ajax({
            url: BASE_URL,
            data: {},
            contentType: "application/json; charset=utf-8",
            dataType: "json"
        });
    };

    $.when(fetch_root()).then(function(data) {
        var promises = fetch_schemas(data.schema_urls);
        $.when.apply(null, promises).then(function(schemas) {
            console.log("DONE", this, schemas);
        });
    });
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
Elf Sternberg
  • 16,129
  • 6
  • 60
  • 68
  • I have almost an identical problem, except I need to fire a "success" method for each ajax query in fetch_one, before "DONE" is printed. How would you go about doing this? I tried using .pipe after "fetch_one", but that didn't seem to work. – CambridgeMike Jul 24 '11 at 17:08

4 Answers4

198

You're looking for

$.when.apply($, promises).then(function(schemas) {
     console.log("DONE", this, schemas);
}, function(e) {
     console.log("My ajax failed");
});

This will also work (for some value of work, it won't fix broken ajax):

$.when.apply($, promises).done(function() { ... }).fail(function() { ... });` 

You'll want to pass $ instead of null so that this inside $.when refers to jQuery. It shouldn't matter to the source but it's better then passing null.

Mocked out all your $.ajax by replacing them with $.when and the sample works

So it's either a problem in your ajax request or the array your passing to fetch_schemas.

luke
  • 36,103
  • 8
  • 58
  • 81
Raynos
  • 166,823
  • 56
  • 351
  • 396
  • Thank you. How is this syntax different from the done().fail()? – Elf Sternberg Feb 02 '11 at 19:47
  • 2
    @elf Sternberg, `.then(a,b) === .done(a).fail(b)` it's a lazy shorthand. You can call `.done(a).fail(b)` if you want – Raynos Feb 02 '11 at 19:48
  • 1
    Oh, and the use of $.when.apply($, ...) and $.when.apply(null, ...) seems to be irrelevant. jQuery itself doesn't have a promise() method, so it gets ignored in favor of an internally generated Deferred object (jQuery 1.5, line 943). – Elf Sternberg Feb 02 '11 at 19:49
  • 1
    @ElfSternberg it is indeed irrelevant, but for readability I don't need to take a second glance at `$.when.apply($, ...`. The `null` makes me go "wait, what?". It's a matter of style and coding practice. I had to read the source to confirm `this` wouldn't throw a null reference inside jQuery.when! – Raynos Feb 02 '11 at 19:51
  • Fair enough. Anyway, yes, I tracked it down to bad json coming in from the outside world. Now I'm on to decorating the array that then() passes to its function. This is fun stuff. – Elf Sternberg Feb 02 '11 at 21:25
  • @ElfSternberg see here for more uses of [`Deferred`](http://stackoverflow.com/questions/4869609/how-can-jquery-deferred-be-used) – Raynos Feb 02 '11 at 21:27
  • 7
    The use of null makes me think 'ok, this is a kind of workaround' (which it is), whereas if $ was used my attention would be diverted to thinking about wtf the $ was for. – Danyal Aytekin Oct 05 '12 at 12:37
  • When would you need to `$.when.apply(somethingElse, ...`? – Adam Barnes Sep 26 '16 at 16:53
53

The workaround above (thanks!) doesn'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($, promises).then(function() {
     var schemas=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 (jQuery.when.all===undefined) {
    jQuery.when.all = function(deferreds) {
        var deferred = new jQuery.Deferred();
        $.when.apply(jQuery, deferreds).then(
            function() {
                deferred.resolve(Array.prototype.slice.call(arguments));
            },
            function() {
                deferred.fail(Array.prototype.slice.call(arguments));
            });

        return deferred;
    }
}

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(promises).then(function(schemas) {
     console.log("DONE", this, schemas); // 'schemas' is now an array
}, function(e) {
     console.log("My ajax failed");
});
crispyduck
  • 1,742
  • 1
  • 12
  • 6
  • @crispyduck - do you know if you can be 100% sure that the order of the array elements in the "schemas" var in then() will always be in the same order as the ajax calls in the "promises" var in the when()? – netpoetica Jun 26 '14 at 13:35
  • 6
    This ought to just be built-into jQuery, but - the jQuery team has rejected the request several times. Meanwhile, people keep asking the question here and opening similar tickets against jQuery and we end up with a userland implementation everywhere and/or awkward calls to `apply()` ... go figure. – mindplay.dk Aug 21 '14 at 11:15
  • Thanks for this solution! Is there a way to get the successful items also if one (or more) failed? – doktoreas Apr 11 '15 at 11:49
  • well all you've done here is hidden `arguments` manipulation into its own method. Great for re-use, but does not address the "ugliness" of having to deal with `arguments` (you could easily have just: `var schemas=Array.prototype.slice.call(arguments);)` – cowbert May 22 '15 at 10:15
  • 2
    @crispyduck, should'nt `deferred.fail(...)` read `deferred.reject(...)`? – Bob S Feb 14 '16 at 19:30
  • Yes it should, because .fail doesn't catch 404's, but .reject does. – t.mikael.d Aug 13 '16 at 14:09
  • THANK You for this solution! It is perfect for handling each response individually when requiring all-or-none to be successful for the next step. – Phil Lucks Jan 10 '18 at 18:14
18

If you are using ES6 version of javascript There is a spread operator(...) which converts array of objects to comma separated arguments.

$.when(...promises).then(function() {
 var schemas=arguments; 
};

More about ES6 spread operator https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator find here

pashaplus
  • 3,596
  • 2
  • 26
  • 25
  • 1
    Yup. Although those of us using Coffeescript or one of its descendents/imitators have had access to that operator for awhile now. – Elf Sternberg Mar 29 '15 at 20:16
-1

extends when with this code:

var rawWhen = $.when
$.when = function(promise) {
    if ($.isArray(promise)) {
        var dfd = new jQuery.Deferred()
        rawWhen.apply($, promise).done(function() {
            dfd.resolve(Array.prototype.slice.call(arguments))
        }).fail(function() {
            dfd.reject(Array.prototype.slice.call(arguments))
        })
        return dfd.promise()
    } else {
        return rawWhen.apply($, arguments)
    }
}
CALL ME TZ
  • 209
  • 1
  • 4
  • 10
  • How is this better than higher-rated answers? What exactly does it do? Answers without explanations aren't very helpful. – Ian Kemp Apr 30 '21 at 18:09