2

I'm working on a client-side JS app which is supposed to read a CSV file, make a few API calls per row, then write the results back out to CSV. The part I'm stuck on is how to orchestrate the requests and fire off a function when all are complete. This is what I have so far:

var requests = [];

// loop through rows
addresses.forEach(function (address, i) {
    // make request
    var addressRequest = $.ajax({
            dataType: 'json',
            url: 'http://api.com/addresses/' + address,
            success: function (data, textStatus, jqXhr) { APP.didGetAddressJson(data, i, jqXhr) },
            error: function (jqXhr, textStatus, errorThrown) { APP.didFailToGetAddressJson(errorThrown, i) },
        });
    requests.push(addressRequest);

    // make some more requests (handled by other success functions)
});

// leggo
$.when.apply($, requests).done(APP.didFinishGeocoding);

The problem is that if one of the rows throws a 404 the done function isn't called. I switched it to always and now it's getting called, but not at the end -- it's usually somewhere in the middle if I log the execution of each callback to the console. However, if I edit the CSV so there are no errors it gets called at the end as expected. Am I doing something here that's allowing the always to fire early?

Update: could it just be that the console's logging it out of order?

serverpunk
  • 10,665
  • 15
  • 61
  • 95
  • In the middle of what? AJAX requests don't necessarily finish in the same order that they were sent. – Barmar Jun 11 '15 at 20:25
  • forEach() on jQuery? – emerson.marini Jun 11 '15 at 20:26
  • https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach – serverpunk Jun 11 '15 at 20:27
  • @Barmar I meant if I log each callback to the console. Updated the question. – serverpunk Jun 11 '15 at 20:30
  • `done` is the same as providing a `success` callback in the options. It's also possible that your requests are completing prior to making it out of the loop. Try adding the `always` prior to pushing the request onto the stack: `addressRequest.always(function() {alert('stuff');}); requests.push(addressRequest);` – ps2goat Jun 11 '15 at 20:31
  • @ps2goat Since JS is single-threaded, async calls can't complete until the function returns. So it doesn't matter when you call `$.when`. – Barmar Jun 11 '15 at 20:33
  • I still don't understand what you mean by "not at the end". The end of what? – Barmar Jun 11 '15 at 20:35
  • @Barmar: I missed that line; I was assuming it was like the Google Analytics queue. – ps2goat Jun 11 '15 at 20:36
  • @Barmar what I meant was not at the end of the console. I'm logging each intermediate callback and then the final `.done`, So I would expect so see something like `gotResponse`, `gotResponse`, `got404`, `gotResponse`, `finishedGettingResponses`. I'm saying that last one isn't appearing at the end. – serverpunk Jun 11 '15 at 20:41
  • 1
    Read the paragraph of https://api.jquery.com/jquery.when/ that explains how it works when multiple deferred objects are passed. Particularly: _The method will resolve its master Deferred as soon as all the Deferreds resolve, or reject the master Deferred as soon as one of the Deferreds is rejected._ So the `.done()` is called as soon as any of the AJAX calls fails, otherwise when all of them succeed. – Barmar Jun 11 '15 at 20:44
  • Sorry, I meant to say `.always`. But it sounds like it works the same. Is there some other event that waits for all the deferreds to finish? – serverpunk Jun 11 '15 at 20:46
  • That's what I meant, too. – Barmar Jun 11 '15 at 20:46
  • I feel slightly less crazy http://stackoverflow.com/questions/5824615/jquery-when-callback-for-when-all-deferreds-are-no-long-unresolved-either-r?rq=1 – serverpunk Jun 11 '15 at 21:12
  • @Jura See http://stackoverflow.com/questions/28131082/jquery-ajax-prevent-fail-in-a-deferred-sequential-loop/ ; post below. – guest271314 Jun 12 '15 at 01:33

2 Answers2

2

You need to prevent error(s) sending the promise returned by $.when.apply($, requests) down the error path.

This can be achieved by :

  • chaining .then() to your $.ajax() calls, rather than specifying "success" and "error" handlers as $.ajax() options.
  • handling errors by converting to success (as this is jQuery, you have to return a resolved promise from the error handler).

This approach also allows you to control the data that's eventually delivered to APP.didFinishGeocoding()

With a few assumptions, the general shape of your code should be as follows :

function foo () {//assume there's an outer function wrapper 
    var errorMarker = '**error**';

    var requests = addresses.map(function (address, i) {
        return $.ajax({
            dataType: 'json',
            url: 'http://api.com/addresses/' + address
        }).then(function (data, textStatus, jqXhr) { //success handler
            return APP.didGetAddressJson(data, i, jqXhr); //whatever APP.didGetAddressJson() returns will appear as a result at the next stage.
        }, function (jqXhr, textStatus, errorThrown) { // error handler
            APP.didFailToGetAddressJson(errorThrown, i);
            return $.when(errorMarker);//errorMarker will appear as a result at the next stage - but can be filtered out.
        });
        // make some more requests (handled by other success functions)
    });

    return $.when.apply($, requests).then(function() {
        //first, convert arguments to an array and filter out the errors
        var results = Array.prototype.slice.call(arguments).filter(function(r) {
            return r !== errorMarker;
        });

        //then call APP.didFinishGeocoding() with the filtered results as individual arguments.
        return APP.didFinishGeocoding.apply(APP, results);

        //alternatively, call APP.didFinishGeocoding() with the filtered results as an array.
        //return APP.didFinishGeocoding(results);
    });
}

Tweak as necessary.

Roamer-1888
  • 19,138
  • 5
  • 33
  • 44
1

Try passing both resolved , rejected jQuery promise object through a whenAll function , filtering resolved, rejected promise object within .then() at completion of whenAll . See also Jquery Ajax prevent fail in a deferred sequential loop


(function ($) {
    $.when.all = whenAll;
    function whenAll(arr) {
        "use strict";
        var deferred = new $.Deferred(),
            args = !! arr 
                   ? $.isArray(arr) 
                     ? arr 
                     : Array.prototype.slice.call(arguments)
                       .map(function (p) {
                         return p.hasOwnProperty("promise") 
                         ? p 
                         : new $.Deferred()
                           .resolve(p, null, deferred.promise())
                       }) 
                   : [deferred.resolve(deferred.promise())],
            promises = {
                "success": [],
                  "error": []
            }, doneCallback = function (res) {
                promises[this.state() === "resolved" 
                         || res.textStatus === "success" 
                         ? "success" 
                         : "error"].push(res);
                return (promises.success.length 
                       + promises.error.length) === args.length 
                       ? deferred.resolve(promises) 
                       : res
            }, failCallback = function (res) {
                // do `error` notification , processing stuff
                // console.log(res.textStatus);
                promises[this.state() === "rejected" 
                        || res.textStatus === "error" 
                        ? "error" 
                        : "success"].push(res);
                return (promises.success.length 
                       + promises.error.length) === args.length 
                       ? deferred.resolve(promises) 
                       : res
            };
        $.map(args, function (promise, index) {
            return $.when(promise).always(function (data, textStatus, jqxhr) {
                return (textStatus === "success") 
                    ? doneCallback.call(jqxhr, {
                        data: data,
                        textStatus: textStatus 
                                    ? textStatus 
                                    : jqxhr.state() === "resolved" 
                                      ? "success" 
                                      : "error",
                        jqxhr: jqxhr
                      }) 
                    : failCallback.call(data, {
                        data: data,
                        textStatus: textStatus,
                        jqxhr: jqxhr
                      })
            })
        });
        return deferred.promise()
    };
}(jQuery));

e.g

var request = function (url) {
    return $.ajax({
                   url: "http://api.com/addresses/" + url, 
                   dataType: "json"
           })
    }
, addresses = [
    ["/echo/json/"], // `success`
    ["/echo/jsons/"], // `error`
    ["/echo/json/"], // `success`
    ["/echo/jsons/"], // `error`
    ["/echo/json/"] // `success`
];

$.when.all(
  $.map(addresses, function (address) {
    return request(address)
  })
)
.then(function (data) {
    console.log(data);
    // filter , process responses
    $.each(data, function(key, value) {
        if (key === "success") {
           value.forEach(function(success, i) {
              console.log(success, i);
              APP.didGetAddressJson(success.data, i, success.jqxhr);
            })
        } else {            
           value.forEach(function(error, i) {
              console.log(error, i);
              APP.didFailToGetAddressJson(error.jqxhr, i)
          })
        }
    })
}, function (e) {
    console.log("error", e)
});

jsfiddle http://jsfiddle.net/guest271314/ev4urod1/

Community
  • 1
  • 1
guest271314
  • 1
  • 15
  • 104
  • 177