jQuery.when - Callback for when ALL Deferreds are no longer 'unresolved' (either resolved or rejected)?

When multiple Deferred objects are passed to jQuery.when, the method returns the Promise from a new "master" Deferred object that tracks the aggregate state of all the Deferreds it has been passed.

The method will either

  1. resolve its master Deferred as soon as ALL the Deferreds resolve, or
  2. reject its master Deferred as soon as ONE of the Deferreds is rejected.

If the master Deferred is resolved (ie. ALL the Deferreds resolve), it is passed the resolved values of all the Deferreds that were passed to jQuery.when. For example, when the Deferreds are jQuery.ajax() requests, the arguments will be the jqXHR objects for the requests, in the order they were given in the argument list:

$.when( $.getJSON('foo'), $.getJSON('bar') ).done(function(foo, bar) {

    // foo & bar are jqXHR objects for the requests

});

In the multiple Deferreds case where one of the Deferreds is rejected, jQuery.when IMMEDIATELY FIRES the fail callbacks for its master Deferred, even if some of the Deferreds may still be unresolved at that point:

$.when( $.getJSON('foo'), $.getJSON('bar') ).fail(function(req) {

    // req is the jqXHR object for one of the failed requests

});

I need to fire a callback when all the Deferreds passed to jQuery.when are no longer 'unresolved' (ie. all are either 'resolved' or 'rejected'). I could send JSON objects with 200 OK codes (instead sending JSON with 404 Not Found error status codes) and determine success/error in the done() method, but I'd prefer keeping my API RESTful. How can I accomplish this?

Answers:

Answer

@Alnitak answer is clever and helped me erase a hack that I'd created in which I was somewhat artificially resolving a promise - regardless of underlying outcome - in order that I may use 'when' to batch up multiple requests and use 'done' to proceed regardless of their success/failure.

I'm "answering" Alnitak's answer in the hope of providing another use for his suggestion that supports an arbitrary number of underlying promises.

var asyncFunc, entity, entities, $deferred, $deferreds;
// ...
foreach (entity in entities) {
    $deferred = $.Deferred();
    $deferreds.push($deferred);
    asyncFunc(entity).done(...).fail(...).always($deferred.resolve);
}
// ...
$.when.apply($, $deferreds).done(...)

This is pseudo-JavaScript but, it should convey the approach. For some arbitrarily sized set of entities, create a deferred ($deferred) for each entity and push it onto an array ($deferreds), make the async call, add done/fail as desired but always include an 'always' that resolves this entity's $deferred. NB the 'always' receives the deferred's resolve function not its invocation.

The 'when' converts the $deferreds array into the argument list for 'when' and, since this set of deferreds is guaranteed to resolve (thanks to the always), it's now possible to define a 'done' that will be invoked once all the async calls complete regardless of whether these succeed/fail.

Answer

My implementation:

Plugin Code:

jQuery.whenAll = function (deferreds) {
        var lastResolved = 0;

        var wrappedDeferreds = [];

        for (var i = 0; i < deferreds.length; i++) {
            wrappedDeferreds.push(jQuery.Deferred());

            deferreds[i].always(function() {
                wrappedDeferreds[lastResolved++].resolve(arguments);
            });
        }

        return jQuery.when.apply(jQuery, wrappedDeferreds).promise();
    };

To use it:

jQuery.whenAll([jQuery.get('/your-resource'), jQuery.get('/your-resource')])
   .done(
       function(result1, result2) {
           console.log(result1[1]);
           console.log(result2[1]);
       });

Check out the fiddle: http://jsfiddle.net/LeoJH/VMQ3F/

Answer

@Alnitak and @DazWilkin answers are great! But I personally prefer functional style so here is a functional version for arbitrary number of promises:

var entities;
// ...
var deferreds = entities.map(function() {
    var deferred = $.Deferred();
    asyncFunc(this).done(...).fail(...).always(deferred.resolve);
    return deferred;
}
// ...
$.when.apply($, deferreds).done(...)

Compared to @DazWilkin answer, I use map function instead of foreach.

Answer

I think the easiest way to do this is to keep a secondary Deferred object around for each AJAX request, and ensure that that one is always resolved:

var d1 = $.Deferred();
var d2 = $.Deferred();

var j1 = $.getJSON(...).complete(d1.resolve);
var j2 = $.getJSON(...).complete(d2.resolve);

$.when(j1, j2).done( only fires if j1 AND j2 are resolved );

$.when(d1, d2).done(function() {
     // will fire when j1 AND j2 are both resolved OR rejected
     // check j1.isResolved() and j2.isResolved() to find which failed
});

This is making use of the additional AJAX .complete() method which jQuery adds to its promises for AJAX methods, which is called for both resolved and rejected promises.

NB: d1.resolve works as a callback in its own right, it doesn't need to be wrapped in a function() { ... } block.

Answer

I've recently made a plugin that may help. I call it $.whenAll.

This extension treats all successes and failures as progress events. After all the promises have completed, the global promise is resolved if there were no errors. Otherwise the global promise is rejected.

$.whenAll - https://gist.github.com/4341799 (tests)

Sample usage:

$.whenAll($.getJSON('foo'), $.getJSON('bar'))
  .then(
    doneCallback
    ,failcallback
    // progress callback
    // the only problem is $.ajax.done/fail states call their callbacks 
    // with params in different locations (except for state)
    ,function(data, state, jqXhr) {
      if (state == 'success') {
        // do happy stuff
      }
      else { // error (fail)
        // `data` is actually the jqXhr object for failed requests
        // `jqXhr` is the text of the error "Not Found" in this example
      }
    }
  )
;
Answer

Here's a jQuery plugin I've made by modifying the actual core code for $.when() to use your semantics. For want of a better name it's called $.myWhen():

(function($) {
  $.myWhen = function( subordinate /* , ..., subordinateN */ ) {
    var i = 0,
      responseValues = Array.prototype.slice.call( arguments ),
      length = responseValues.length,

      // the count of uncompleted subordinates
      remaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0,

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

      // Update function for all resolve, reject and progress values
      updateFunc = function( i, contexts, values ) {
        return function( value ) {
          contexts[ i ] = this;
          values[ i ] = arguments.length > 1 ? Array.prototype.slice.call( arguments ) : value;
          if( values === progressValues ) {
            deferred.notifyWith( contexts, values );
          } else if ( !( --remaining ) ) {
            deferred.resolveWith( contexts, values );
          }
        };
      },

      progressValues, progressContexts, responseContexts;

    // add listeners to Deferred subordinates; treat others as resolved
    if ( length > 1 ) {
      progressValues = new Array( length );
      progressContexts = new Array( length );
      responseContexts = new Array( length );
      for ( ; i < length; i++ ) {
        if ( responseValues[ i ] && jQuery.isFunction( responseValues[ i ].promise ) ) {
          responseValues[ i ].promise()
            .always( updateFunc( i, responseContexts, responseValues ) )
            .progress( updateFunc( i, progressContexts, progressValues ) );
        } else {
          --remaining;
        }
      }
    }

    // if we're not waiting on anything, resolve the master
    if ( !remaining ) {
      deferred.resolveWith( responseContexts, responseValues );
    }

    return deferred.promise();
  };
})(jQuery);

Just put this code right after where you've loaded jQuery and the $.myWhen() function will be available alongside $.when(). Everything else is 100% exactly the same except for the semantics.

Answer

An improvement on Leo Hernandez's solution for more general use cases that don't simply involve fetching resources from a server, which for example can include events triggered by user interactions, or asynchronous jQuery UI calls (e.g. slideUp() and slideDown()). See https://jsfiddle.net/1trucdn3/ for enhanced use case.

$.whenAll = function (deferreds) {
    var lastResolved = 0;
    var wrappedDeferreds = [];

    for (var i = 0; i < deferreds.length; i++) {
        wrappedDeferreds.push($.Deferred());
        if (deferreds[i] && deferreds[i].always) {
            deferreds[i].always(wrappedDeferreds[lastResolved++].resolve);
        } else {
            wrappedDeferreds[lastResolved++].resolve(deferreds[i]);
        }
    }

    return $.when.apply($, wrappedDeferreds).promise();
};

The improvement allows us to pass in non-Deferred values into the array argument. This was something that you could do with $.when(). Also, I cleaned up the output you get in the callback function to be more inline with how the original $.when() method works, in case you just want to get back the result regardless of the status. Leo's solution would pass the entire deferred object as a result, which you then need to dig into to find the information you need.

$.whenAll([1, $.Deferred().resolve("Good"), $.Deferred().reject("Bad")])
    .done(function (result1, result2, result3) {
        // result1 -> 1
        // result2 -> "Good"
        // result3 -> "Bad"
    });
Answer

I found a solution where I have 2 requests in a when and am able to access individual successes even when one of the requests fail:

        $.when
        (
            $.getJSON(...).then(function (results)
            {
                console.log('SUCCESS REQUEST 1 BY ITSELF', results);
            }),
            $.getJSON(...).then(function (results)
            {
                console.log('SUCCESS REQUEST 2 BY ITSELF', results);
            })
        ).then
        (
            function (results1, results2)
            {
                console.log('BOTH REQUESTS SUCCESSFUL...');
                console.log('results1', results1);
                console.log('results2', results2);
            },
            function (error1, error2)
            {
                console.log('AT LEAST 1 REQUEST FAILED...');
                console.log('error1', error1);
                console.log('error2', error2);                  
            }
        );

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.