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.
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));
})
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:
function getWebServiceResults() {...}
.callWebService()
is assumed to accept item
- ie the contents of each element of myList
.checkIfContinuable()
must accept at least one argument. It is assumed to accept errorThrown
but might equally accept jqXHR or textStatus.If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With