$.when()
is designed to immediately execute .fail()
if any of the promises passed to it is rejected. Thus, if a1 or a2 is rejected, it will call .fail()
and probably not yet have the data for a3 and a4. That's just how it is designed which does limit its usefulness in some cases. If you want to know each individual outcome regardless of whether one of the other promises fails and you're using $.when()
, then as you surmised, you will need to attached your own .done()
and .fail()
handlers to each individual promise.
In a coincidence for you, I have implemented (for my own use) a MultiDeferred object that allows you to specify whether you want it to fail immediately if any promise fails (as $.when()
does) or wait until all promises have reached their final outcome before calling the master .fail()
or .done()
. This might be useful for you.
This is a summary of the difference in features vs. $.when()
:
- You can configure it to not call it's own
.fail()
until all promises have finished with the .setFailImmediate(false)
method.
- It has a
.add()
method so you can add more promises to it (before it is resolved). Since these promises are often collected in a loop, this makes it easier to just .add()
one rather than collect them all in an array that you can then pass to $.when()
.
.progress()
works differently. Rather than echo the .progress()
of each individual promise, this does a .notify()
which triggers a .progress()
when each individual promise is either rejected or resolved so you can track each individual completion. If you want the actual progress of each individual promise, you can always just attach to them directly.
- You can query the deferred at any time as to whether any promises have failed yet with
.getFailYet()
.
- You can query how many promises have yet to completed with
.getRemaining()
.
Note the constructor $.MultiDeferred()
returns an actual Deferred object (with a few extra methods added) and you can also call .promise()
on it to get just the promise object (that will also have extra methods on it).
The MultiDeferred
object can be used like this:
var d = $.MultiDeferred(a1, a2, a3, a4);
// configure it to not fail immediately when any single promise fails
d.setFailImmediate(false);
// you can optionally add more promises here
// often in a loop
d.add(a5, a6);
d.done(function() {
// called when all promises have been successfully resolved
}).fail(function() {
// even if one promise failed, all the data from all the promises
// is passed here as this can be configured to wait for all
// promises to finish before calling .fail()
});
Working demo: http://jsfiddle.net/jfriend00/3JN52/
The code for the MultiDeferred object is here:
jQuery.MultiDeferred = function(/* zero or more promises */) {
// make the Deferred
var self = jQuery.Deferred();
// get the promise so new methods can be added to it too
var promise = self.promise();
var remainingToFinish = 0;
// two parallel arrays that contain the promises that are added
// and the arguments passed to reject() or resolve()
// so when we call the master reject() or resolve(), we can pass all those args on through
var promises = [];
var args = [];
// keep track of what to do upon failure and whether we had any failures
var anyFail = false;
var failImmediate = false;
function _add(p) {
// save our index in a local variable so it's available in this closure later
// so we know what the array index was of this particular promise when it fires
var index = promises.length;
// save this promise into our array structure
promises.push(p);
// push placeholder in the args array
args.push([null]);
// one more waiting to finish
++remainingToFinish;
// see if all the promises are done
function checkDone(fail) {
return function() {
anyFail |= fail;
// make copy of arguments so we can save them
args[index] = Array.prototype.slice.call(arguments, 0);
--remainingToFinish;
// send notification that one has finished
self.notify.apply(self, args[index]);
// if all promises are done, then resolve or reject
if (self.state() === "pending" && (remainingToFinish === 0 || (fail && failImmediate))){
var method = anyFail ? "reject" : "resolve";
self[method].apply(self, args);
}
}
}
// add our own monitors so we can collect all the data
// and keep track of whether any fail
p.when(checkDone(false), checkDone(true));
}
self.add = function(/* one or more promises or arrays of promises */) {
if (this.state() !== "pending") {
throw "Can't add to a deferred that is already resolved or rejected";
}
var arg;
for (var i = 0; i < arguments.length; i++) {
// if the next arg is not an array, then put it in an array
// so the rest of the code can just treat it like an array
arg = arguments[i];
if (!Array.isArray(arg)) {
arg = [arg];
}
for (var j = 0; j < arg.length; j++) {
_add(arg[j]);
}
}
return this;
}
// set whether the deferred should reject immediately if any subordinate
// promises fails or whether it should wait until all have completed
// default is to wait until all have completed
self.setFailImmediate = function(failQuick) {
failImmediate = failQuick;
return this;
}
// only query methods are on the promise object
// get count of remaining promises that haven't completed yet
self.getRemaining = promise.getRemaining = function() {
return remainingToFinish;
}
// get boolean on whether any promises have failed yet
self.getFailYet = promise.getFailYet = function() {
return anyFail;
}
// add any promises passed to the constructor
self.add.apply(self, arguments);
return self;
};