Can jQuery deferreds be cancelled?
Solution 1
Looking in the jQuery doc and code, I don't see any way to cancel a jQuery deferred.
Instead, you probably need a way in your resolveWith
handler to know that a subsequent ajax call has already been fired and this ajax call should ignore its result. You could do that with a globally incrementing counter. At the start of your ajax call, you increment the counter and then you grab the value into a local variable or put it as a property on the ajax object. In your resolveWith
handler, you check to see if the counter still has the same value as when your ajax call started. If not, you ignore the result. If it does, no new ajax calls have been fired so you can process the result.
Alternately, you could refuse to fire a new ajax call while one is in flight already so you never had more than one in flight at a time. When the one finishes, you could either just use that result or fire the next one if desired.
Solution 2
While you can't "cancel" a deferred like you want, you could create a simple closure to keep track of the last ajax call through $.ajax returning an jqXHR object. By doing this you can simply abort() the call when a new jqXHR comes in to play if the last one wasn't finished. In your code's case it will reject the jqXHR and leave the deferred open to be deleted as you initially wanted.
var api = (function() {
var jqXHR = null;
return function(options) {
var url = options.url;
if (jqXHR && jqXHR.state() === 'pending') {
//Calls any error / fail callbacks of jqXHR
jqXHR.abort();
}
var deferred = $.Deferred(function() {
this.done(options.success);
this.fail(options.error);
});
jqXHR = $.ajax({
url: url,
data: options.toSend,
dataType: 'jsonp'
});
jqXHR.done(function(data, textStatus, jqXHR) {
if (data.f && data.f !== "false") {
deferred.resolve();
} else {
deferred.reject();
}
});
//http://api.jquery.com/deferred.promise/
//keeps deferred's state from being changed outside this scope
return deferred.promise();
};
})();
I've posted this on jsfiddle. If you wish to test it out. Set timeout is used in combination with jsfiddles delayer to simulate a call being interupted. You'll need a console enabled browser to see the logs.
On a side note switch any .success(), .error(), and complete() methods over to deferred methods done(), fail(), and always(). Via jquery/ajax
Deprecation Notice: The jqXHR.success(), jqXHR.error(), and jqXHR.complete() callbacks will be deprecated in jQuery 1.8. To prepare your code for their eventual removal, use jqXHR.done(), jqXHR.fail(), and jqXHR.always() instead as newer
Solution 3
JustinY: seems like you're really close already to what you want. You're already using two deferreds (inner- > the ajax and outer -> $.Deferred()). You're then using the inner deferred to decide how to resolve the outer deferred based on some conditions.
Well, so just don't resolve the outer deferred at all when you don't want to (maybe you have a boolean variable that serves as a toggle gate for allowing the inner dfd to resolve/reject at all). Nothing bad will happen: whatever handlers you have attached to this entire function won't fire. Example in your inner success function:
if(gateOpen){
gateOpen = false;
if(hasStatus(jsonReturn, 'code', 200)) {
deferred.resolveWith(this, [jsonReturn]);
}
else {
deferred.rejectWith(this, [jsonReturn]);
}
}
Some other logic in the application will decide when the gateOpen gets set back to true (some sort of _.throttle() or _.debounce() timeout, user interaction, whatever you want).If you wanted to track or cancel other requests in the else of that function, you could do that too. But the basic thing is that you don't have to resolve OR reject that outer deferred. And that's the same as canceling it, even if you don't cancel/abort the inner one.
Solution 4
I've created a shim that seamlessly adds the ability to cancel deferred objects and ajax requests.
In short, once a deferred object has been canceled, resolutions/rejections are completely ignored, and the state
becomes "canceled".
According to jQuery.com, "Once the object has entered the resolved or rejected state, it stays in that state." Therefore, attempts to cancel are ignored once a deferred object is resolved or rejected.
(function () {
originals = {
deferred: $.Deferred,
ajax: $.ajax
};
$.Deferred = function () {
var dfr = originals.deferred(),
cancel_dfr = originals.deferred();
dfr.canceled = false;
return {
cancel: function () {
if (dfr.state() == 'pending') {
dfr.canceled = true;
cancel_dfr.resolve.apply(this, arguments);
}
return this;
},
canceled: cancel_dfr.done,
resolve: function () {
if ( ! dfr.canceled) {
dfr.resolve.apply(dfr, arguments);
return this;
}
},
resolveWith: function () {
if ( ! dfr.canceled) {
dfr.resolveWith.apply(dfr, arguments);
return this;
}
},
reject: function () {
if ( ! dfr.canceled) {
dfr.reject.apply(dfr, arguments);
return this;
}
},
rejectWith: function () {
if ( ! dfr.canceled) {
dfr.rejectWith.apply(dfr, arguments);
return this;
}
},
notify: function () {
if ( ! dfr.canceled) {
dfr.notify.apply(dfr, arguments);
return this;
}
},
notifyWith: function () {
if ( ! dfr.canceled) {
dfr.notifyWith.apply(dfr, arguments);
return this;
}
},
state: function () {
if (dfr.canceled) {
return "canceled";
} else {
return dfr.state();
}
},
always : dfr.always,
then : dfr.then,
promise : dfr.promise,
pipe : dfr.pipe,
done : dfr.done,
fail : dfr.fail,
progress : dfr.progress
};
};
$.ajax = function () {
var dfr = $.Deferred(),
ajax_call = originals.ajax.apply(this, arguments)
.done(dfr.resolve)
.fail(dfr.reject),
newAjax = {},
ajax_keys = [
"getResponseHeader",
"getAllResponseHeaders",
"setRequestHeader",
"overrideMimeType",
"statusCode",
"abort"
],
dfr_keys = [
"always",
"pipe",
"progress",
"then",
"cancel",
"state",
"fail",
"promise",
"done",
"canceled"
];
_.forEach(ajax_keys, function (key) {
newAjax[key] = ajax_call[key];
});
_.forEach(dfr_keys, function (key) {
newAjax[key] = dfr[key];
});
newAjax.success = dfr.done;
newAjax.error = dfr.fail;
newAjax.complete = dfr.always;
Object.defineProperty(newAjax, 'readyState', {
enumerable: true,
get: function () {
return ajax_call.readyState;
},
set: function (val) {
ajax_call.readyState = val;
}
});
Object.defineProperty(newAjax, 'status', {
enumerable: true,
get: function () {
return ajax_call.status;
},
set: function (val) {
ajax_call.status = val;
}
});
Object.defineProperty(newAjax, 'statusText', {
enumerable: true,
get: function () {
return ajax_call.statusText;
},
set: function (val) {
ajax_call.statusText = val;
}
});
// canceling an ajax request should also abort the call
newAjax.canceled(ajax_call.abort);
return newAjax;
};
});
Once added, you may cancel an ajax call:
var a = $.ajax({
url: '//example.com/service/'
});
a.cancel('the request was canceled');
// Now, any resolutions or rejections are ignored, and the network request is dropped.
..or a simple deferred object:
var dfr = $.Deferred();
dfr
.done(function () {
console.log('Done!');
})
.fail(function () {
console.log('Nope!');
});
dfr.cancel(); // Now, the lines below are ignored. No console logs will appear.
dfr.resolve();
dfr.reject();
Admin
Updated on June 06, 2022Comments
-
Admin almost 2 years
I have a situation where I want to cancel a deferred. The deferred is associated with an ajax call.
Why I am using deferreds
I don't use the normal xhr objects returned by $.ajax. I'm using jsonp, which means I can't use HTTP status codes for error handling and have to embed them in the responses. The codes are then examined and an associated deferred object is marked as resolved or rejected accordingly. I have a custom api function that does this for me.
function api(options) { var url = settings('api') + options.url; var deferred = $.Deferred(function(){ this.done(options.success); this.fail(options.error); }); $.ajax({ 'url': url, 'dataType':'jsonp', 'data': (options.noAuth == true) ? options.data : $.extend(true, getAPICredentials(), options.data) }).success(function(jsonReturn){ // Success if(hasStatus(jsonReturn, 'code', 200)) { deferred.resolveWith(this, [jsonReturn]); } // Failure else { deferred.rejectWith(this, [jsonReturn]); } }); return deferred; }
Why I want to cancel the deferred
There is an input field that serves as a filter for a list and will automatically update the list half a second after typing ends. Because it is possible for two ajax calls to be outstanding at a time, I need to cancel the previous call to make sure that it doesn't return after the second and show old data.
Solutions I don't like
- I don't want to reject the deferred because that will fire handlers attached with
.fail()
. - I can't ignore it because it will automatically be marked as resolved or rejected when the ajax returns.
- Deleting the deferred will cause an error when the ajax call returns and tries to mark the deferred as resolved or rejected.
What should I do?
Is there a way to cancel the deferred or remove any attached handlers?
Advice on how to fix my design is welcome, but preference will be given to finding a way to remove handlers or prevent them from firing.
- I don't want to reject the deferred because that will fire handlers attached with
-
zerkms almost 12 yearsI'd better go with
rejectWith
, but +1 for the idea -
jfriend00 almost 12 years@zerkms - it depends upon where you want to do the duplicate detection. If that happens at the point where the code is deciding whether to reject or resolve, then perhaps rejectWith is a good idea with an argument indicating it's rejected because of a subsequent call. If the dup detection happens in the resolved handler, then it's already been resolved so rejecting it isn't feasible.
-
zerkms almost 12 yearsAnd as another proposal: instead of counter I'd just pass
{ manuallyAborted: true }
2 argument torejectWith
, since counter may cause race conditions in true multi-threading environments, which is not applicable to browsers and JS, at least yet ;-) -
jfriend00 almost 12 years@zerkms - the counter is used to detect multiple ajax calls in flight when a result comes in. If you get rid of the counter, how would you detect that?
-
zerkms almost 12 yearswell, I assumed that OP wants to perform
xhr.abort()
to cancel unnecessary request. From this point of view I see more logical place to put that logic - isfail
handlers -
zerkms almost 12 yearsI would just check if
{ manuallyAborted: true }
passed to thefail
handler -
jfriend00 almost 12 years@zerkms - OK, you made different assumptions than I did. The OP didn't say anything about doing an
abort()
. -
zerkms almost 12 years"I need to cancel the previous call to make sure that it doesn't return after the second and show old data".
-
jfriend00 almost 12 years@zerkms - My proposal cancelled it's effect by ignoring the result - different interpretation. The OP can choose which way they want to go.
-
ilovett over 10 yearsI believe
jqXHR.abort()
triggers thefail
function withxhr.status === 0
, so you may want to alsoreject()
your deferred inside of thefail
/error
function. -
benzkji almost 10 years@zerkms request aborted with xhr.abort() may be out of sight in jQuery, but nevertheless the server will process till the end - design of http protocol!
-
zerkms almost 10 years@benzkji: "design of http protocol" --- could you please prove it with any reference to HTTP RFC that requires a server to finish process even if the connection was terminated? I haven't seen such a statement there ever.
-
jfriend00 almost 10 years@benzkji - it's not the design of the http protocol. It is totally up to the server process for whether it does anything differently if the connection drops or if the main action on the server is ever even informed. An http server could abort its process when the connection was dropped, but it is more likely that once the command is received from the client, that command is processed and only when it goes to send the response back does it realize that the connection has droppped. It doesn't have to be that way, but that is the simplest server-side behavior.
-
benzkji almost 10 yearsas Erv Walter states in his comment on the accepted answer: stackoverflow.com/questions/446594/… (my "explanation" was a bit vague, I agree). to elaborate a bit: I think that jquery will not tell the server to terminate the connection, but just stop listening to it's response, at least this is my experience. no RFC, sorry.
-
jfriend00 almost 10 years@benzkji - I just stepped through the
xhr.abort()
method that jQuery provides for aborting an ajax call that is in progress. It does follow through and call theXMLHttpRequest.abort()
method which is supposed to terminate the connection. As theXMLHttpRequeset
object is only now getting a draft standard, I can't say what individual browsers are actually doing in that regard. The Chrome network trace shows that the ajax call is actually cancelled. As I think everyone now understands, cancelling the request doesn't force the server to do anything - it may still process the request. -
benzkji almost 10 years@jfriend00 thx for that research - as you say, it's a draft. as for myself, thinking that "canceling" the request could avoid load on a server, this is, almost always today, not true...so I think it's important to keep that "detail" in mind.
-
jfriend00 almost 10 years@benzkji - the draft spec for the XMLHttpRequest object has absolutely nothing to do with the server behavior. Once the server receives the request and starts processing it, it is unlikely to even know the socket has been closed until it starts sending back the response. That would be the normal server process flow. While a server could implement more advanced flow and somehow abort processing if the socket is closed while the processing is happening, that would require special design in the server process and would rarely (perhaps never) be the default behavior.
-
Ryan Q over 8 yearsThanks for the heads up Artur. It would be helpful to mention why it won't work for the OP to edit or make a suggestion for the change.