Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Continue of failure of jQuery Deferred chain

I'm doing a series of sequential AJAX calls in jQuery, using the usual method of chaining with Deferred. The first call returns a list of values and the subsequent calls are made with those returned list entries. After the first call that returns the list, the subsequent calls may be done in any order, but they must be done one at a time. So this is what I use:

$.when(callWebService()).then(
function (data) {
    var looper = $.Deferred().resolve(),
        myList = JSON.parse(data);
    for (var i in myList) {
        (function (i) {
            looper = looper.then(function () { // Success
                return callWebService();
            }, 
            function (jqXHR, textStatus, errorThrown) { // Failure
                if (checkIfContinuable(errorThrown) == true)
                    continueChain();
                else
                    failWithTerribleError();
            });
        })(i);
    }
});

It turns out that the subsequent calls may fail at times, but I still want to do the remainder of the calls. In my listing, that's what this little bit of inventive pseudo code is meant to do:

if (checkIfContinuable(errorThrown) == true)
    continueChain();
else
    failWithTerribleError();

How on earth do I implement continueChain though? It appears as though a failure on any deferred will cause the rest of the chain to also fail. Instead, I'd like to log the error and continue with the rest of the list.

like image 316
Cobus Kruger Avatar asked Oct 20 '15 21:10

Cobus Kruger


2 Answers

With Promises/A+ this is as easy as

promise.then(…, function(err) {
    if (checkIfContinuable(err))
        return valueToConinueWith;
    else
        throw new TerribleError(err);
})

Unfortunately, jQuery is still not Promises/A+ compliant, and forwards the old value (result or error) - unless you return a jQuery Deferred from the callback. This works just the same way as rejecting from the success handler:

jDeferred.then(…, function(err) {
    if (checkIfContinuable(err))
        return $.Deferred().resolve(valueToConinueWith);
    else
        return $.Deferred().reject(new TerribleError(err));
})
like image 112
Bergi Avatar answered Oct 21 '22 02:10

Bergi


Recovering from an error in a jQuery promise chain is more verbose than with Promises/A+ implementations, which naturally catch errors in a .catch's or a .then's error handler. You have to throw/rethrow in order to propagate the error state.

jQuery works the other way round. A .then's error handler (.catch doesn't exist) will naturally propagate the error state. To emulate "catch", you have to return a resolved promise, and the chain will progress down its success path.

Starting with an array of items on which you want to base a series of async calls, it's convenient to use Array.prototype.reduce().

function getWebServiceResults() {
    return callWebService().then(function(data) {
        var myList;

        // This is genuine Javascript try/catch, in case JSON.parse() throws.
        try {
            myList = JSON.parse(data);
        }
        catch (error) {
            return $.Deferred().reject(error).promise();//must return a promise because that's what the caller expects, whatever happens.
        }

        //Now use `myList.reduce()` to build a promise chain from the array `myList` and the items it contains.
        var promise = myList.reduce(function(promise, item) {
            return promise.then(function(arr) {
                return callWebService(item).then(function(result) {
                    arr.push(result);
                    return arr;
                }, function(jqXHR, textStatus, errorThrown) {
                    if(checkIfContinuable(errorThrown)) {
                        return $.when(arr); // return a resolved jQuery promise to put promise chain back on the success path.
                    } else {
                        return new Error(textStatus);//Although the error state will be naturally propagated, it's generally better to pass on a single js Error object rather than the three-part jqXHR, textStatus, errorThrown set.
                    }
                });
            });
        }, $.when([])) // starter promise for the reduction, resolved with an empty array

        // At this point, `promise` is a promise of an array of results.

        return promise.then(null, failWithTerribleError);
    });
}

Notes:

  • An overall function wrapper is assumed, function getWebServiceResults() {...}.
  • callWebService() is assumed to accept item - ie the contents of each element of myList.
  • To do its job, checkIfContinuable() must accept at least one argument. It is assumed to accept errorThrown but might equally accept jqXHR or textStatus.
like image 45
Roamer-1888 Avatar answered Oct 21 '22 00:10

Roamer-1888