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:
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.
Related
I have some code that handles Angular promises like this
somethingThatReturnsAPromise
.then(function (data) {
// handle success
})
.catch(function (error) {
// handle error
})
.finally(function () {
// always do this
});
I understand this syntax is deprecated now, and this code should be replaced with
somethingThatReturnsAPromise.then(
function (data) {
// handle success
},
function (error) {
// handle error
}
);
But where should I put the code that was previously in finally when using this new syntax, i.e. code that is executed both when the promise is resolved (successful) and rejected (fails)?
If you want to go with then either way, you can provide a handler for both success and error promise handlers:
function always() {
// Do whatever either it fails or succeeds
}
somethingThatReturnsAPromise.then(always, always).then(function(data) {
}, function(error) {
});
1st: I didn't find anything about any (Promise-related) method being deprecated in the official docs.
2nd: finally is way more complicated then a then(cb, cb), since it doesn't catch Errors, and doesn't propagate the results of your callback, but if you return a promise, it waits for this promise to resolve until it continues propagating the current value.
Sth like this:
function _finally(promise, callback) {
var handleValue = isError => value => $q((resolve, reject) => {
//call your callback
//if this throws, propagate the Error
var w = typeof callback === "function" && callback();
//prepare to push the current value/error
var fn = isError?
() => reject(value):
() => resolve(value);
//check wether your callback has returned sth. Promise-like
if(w && typeof w.then === "function"){
//then we'll wait for this to resolve,
//before we continue propagating the current value/error
w.then(fn, fn);
}else{
//otherwise propagate the current value/error emmediately
fn();
}
});
return $q.resolve(promise).then(
handleValue(false),
handleValue(true)
);
}
I wrote this code only to give you an implession what finally does. Angular's implementation is smoother, so stick with that.
I don't see any reason to think that catch or finally are deprecated or will ever be.
I'm a bit confused I am using the result of callone() to modify a global object(I'm not sure of a better way to do this) trying to accomplish this with deferred. By the time time I calltwo() the global object should be modified with the new data
var obj = {};
var id = obj.id;
//global object
$.when(callone(obj)).then(calltwo(id),function(data)
{
});
- Ajax function :1
function callone(requiredData)
{
var d = new $.Deferred();
var ajaxCall1 = $.ajax({
type:"POST",
url: 'AB/',
data: requiredData,
success: function(data) {
//return data to the callee?
d.resolve(p_obj);
//set ID on the object
obj.id = data.id;
return obj;
},
error: function(jqXHR, textStatus, errorThrown) {
alert(textStatus + ': ' + errorThrown);
},
always: function(data) { }
});
}
function calltwo(id from callback one)
{
}
I've included a much simpler implementation below.
callone() must return a deferred or promise for you to wait on it or chain other operations to it and you can just use the promise that $.ajax() already returns rather than creating your own.
Further, there is no reason to use $.when() here because it really only adds value when you're trying to wait on multiple promises running in parallel which isn't your case at all. So, you can just use the .then() handler on the individual promises you already have.
In addition, you really don't want to use globals when processing async operations. You can chain the promises and pass the data right through the promises.
Here's what callone() should look like:
function callone(requiredData) {
return $.ajax({
type: "POST",
url: 'AB/',
data: requiredData
});
}
function calltwo(...) {
// similar to callone
// returns promise from $.ajax()
}
callone(...).then(function(data) {
// when callone is done, use the id from its result
// and pass that to calltwo
return calltwo(data.id);
}).then(function(data) {
// process result from calltwo here
}, function(err) {
// ajax error here
});
Notice that this code isn't creating any new Deferred objects. It's just using the promise that is already returned from $.ajax(). Note also that it isn't use success: or error: handlers either because those also come through the promises.
Also, note that return a promise from within a .then() handler automatically chains it into the previous promise so the previous promise won't be resolved until the newly returned promise is also resolved. This allows you to keep the chain going.
Also, note that returning data from an async callback function does not return data back to the caller of the original function so your attempt to return something from the success: handler was not accomplishing anything. Instead, use promises and return data through the promises as they are specifically designed to get async data back to the .then() handlers.
Below are my Multiple ajax calls with promises.
$(window).load(function(){
$.when(getApiModemList()).done(function(vendors){
var deferreds = calculateApiBalances(vendors);
$.when.apply($,deferreds).done(function(balance) {
console.log(balance);
console.log("All Done");
});
});
function getApiModemList(){
return $.getJSON("url");
}
function calculateApiBalances(vendors)
{
var defer=[];
$.each(vendors,function(k,v){
defer.push($.getJSON(someurl));
});
return defer;
}
});
Function calculateApiBalances() return me some balance which I need to Sum up to get total of all balance .
But when print console.log(balance) it doesnot provide me with valid array of balance json.
Another issue is if any one of my ajax calls in calculateApiBalances() fails it doesnot prints All Done.
What should be done in above code to achieve this.
But when print console.log(balance) it doesnot provide me with valid array of balance json.
That's a weird thing of $.when. It doesn't provide you with an array, but calls your callback with multiple arguments.
Another issue is if any one of my ajax calls in calculateApiBalances() fails it doesnot prints All Done.
Yes, because when one promise fails the whole $.when() promise is rejected immediately, and you don't have any error handler. You'll have to catch errors individually if you want to always get an array (of possibly invalid responses). See also $.Deferred: How to detect when every promise has been executed.
What should be done in above code to achieve this.
First of all, avoid the deferred antipattern :-)
$(window).load(function() {
getApiModemList().then(calculateApiBalances).then(function() {
var balance = Array.prototype.slice.call(arguments);
console.log(balance);
console.log("All Done");
});
});
function getApiModemList() {
return $.getJSON(somurl).then(function(res) {
return res.data;
});
}
function calculateApiBalances(vendors) {
return $.when.apply($, $.map(vendors, function(v, k) {
return $.getJSON(someurl).then(null, $.when);
}));
}
EDIT by Roamer:
And here's a version of the master routine with a mechanism for summing the balances.
getApiModemList().then(calculateApiBalances).then(function() {
var sumOfBalances = Array.prototype.reduce.call(arguments, function(tot, obj) {
return tot + (+obj.balance || 0);
}, 0);
console.log(sumOfBalances);
console.log("All Done");
});
obj is the object promised by $.getJSON(someurl) in calculateApiBalances(). In the case of a $.getJSON() error, obj will be a jqXHR object, obj.balance will be undefined and +obj.balance will be NaN, therefore default to adding zero; otherwise add obj.balance .
If you wanted to know how many of the getJSON requests were successful and how many were unsuccessful, then there's some more code to write, but not a lot.
I am having trouble interpreting what the Promises/A+ spec meant by this...
https://promisesaplus.com/#point-23
If you have a promise and call .then with one argument, does that mean that the single argument will be called regardless of success or failure?
I feel like it could go either way with interpreting this. I guess the library I would be most concerned with is Q.
The first argument to a .then() handler, whether it is the only argument or not, is always the fulfilled handler and is only called if the promise is fulfilled. That first argument is not treated differently if there is or is not a second argument passed.
So to anyone who is unsure, I made a test script that you can run in your node REPL. The definitive answer is that no it will not call the success with an err attachment.
var Q = require('q');
//call with whether or not the promise will resolve
function aPromise (bool) {
var def = Q.defer();
if (!bool) {
def.reject("oooo0o0o00o0 rejected");
}
def.resolve('winner winner chicken dinner');
return def.promise
}
var whatGonnaBe = aPromise(false)
//The Example
whatGonnaBe
.then(function (response) {
console.log('1')
console.log(JSON.stringify(response) + '1');
})
.then(function (response) {
console.log('2')
console.log(JSON.stringify(response) + '2');
},
function (response) {
console.log('3')
console.log(JSON.stringify(response) + '3');
})
No, the first argument is always the "success" or "resolved" argument. The second argument is always the failure argument, and you can omit it when chaining promises together and have a single "fail" argument to handle all fails. This code is just conceptual:
promise1.then(function() {
return promise2;
}).then(function() {
return promise3;
}).then(function() {
/* everything succeeded, move forward */
}, function() {
/* catch any failure by any of the promises */
});
I'm using the below code to get JSON from multiple urls. However, when one of the URL failed or get 404 response the function the execute doesn't work. I read the jquery doc and I know "then" should execute no matter one of the call has failed.
var data = {};
var calls = [];
for (var i in funcs) {
calls.push(
$.getJSON(base_url+i,
(function(i) {
return function(d) {
data[i] = d;
};
}(i))
)
);
}
$.when.apply($,calls).then(function() {
do_something(data);
});
Take a look at always method. It will executed in both cases.
For example:
$.when.apply($, calls).always(function() {
alert('Resolved or rejected');
});
In response to successful transaction, arguments are same as .done() (ie. a = data, b = jqXHR) and for failed transactions the arguments are same as .fail() (ie. a = jqXHR, b = errorThrown). (c)
I read the jquery doc and I know "then" should execute no matter one of the call has failed.
Nope, the promise only gets fulfilled if all of the passed objects are fulfilled. If one of them fails, the result will get rejected.
is there a way to make do_something(data); execute no matter if it failed or not.
You could use .always:
// doSomething waits until all are fulfilled or one is rejected
$.when.apply($,calls).always(do_something);
Yet, you probably want to execute the callback when all calls are resolved (no matter if fulfilled or rejected) - like allSettled does in Q. With jQuery, you have to work around a little:
var calls = $.map(funcs, function(_, i) {
var d = new $.Deferred;
$.getJSON(base_url+i).done(function(r/*…*/) {
d.resolve(i, r);
}, function(/*…*/) {
d.resolve();
});
return d.promise();
});
$.when.apply($, calls).then(function() {
var data = {};
for (var i=0; i<arguments.length; i++)
data[arguments[i][0]] = arguments[i][1];
do_something(data);
});
As jQuery docs the .then function takes two (or three) arguments in your case deferred.then( doneCallbacks, failCallbacks )
So you need to specify the second function to handle the failing request.
i.e.
$.when.apply($,calls).then(function() {
alerT('ok');
}, function() {
alert('fail');
});
Here a simple fiddle: http://jsfiddle.net/rrMwr/
Hope this helps
You need to use the second parameter of deferred.then() (documentation):
$.when.apply($, calls).then(function() {
do_something(data);
}, function() {
// something failed
});
If you want to call the same callback regardless of successes and failures, use deferred.always() (documentation):
$.when.apply($, calls).always(function() {
do_something(data);
});
It is also worth reading the jQuery.when() documentation, which explains the aggregation when multiple deferred objects are passed.