Can jQuery deferreds be cancelled?

19,266

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();
Share:
19,266
Admin
Author by

Admin

Updated on June 06, 2022

Comments

  • Admin
    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.

  • zerkms
    zerkms almost 12 years
    I'd better go with rejectWith, but +1 for the idea
  • jfriend00
    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
    zerkms almost 12 years
    And as another proposal: instead of counter I'd just pass { manuallyAborted: true } 2 argument to rejectWith, since counter may cause race conditions in true multi-threading environments, which is not applicable to browsers and JS, at least yet ;-)
  • jfriend00
    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
    zerkms almost 12 years
    well, 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 - is fail handlers
  • zerkms
    zerkms almost 12 years
    I would just check if { manuallyAborted: true } passed to the fail handler
  • jfriend00
    jfriend00 almost 12 years
    @zerkms - OK, you made different assumptions than I did. The OP didn't say anything about doing an abort().
  • zerkms
    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
    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
    ilovett over 10 years
    I believe jqXHR.abort() triggers the fail function with xhr.status === 0, so you may want to also reject() your deferred inside of the fail / error function.
  • benzkji
    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
    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
    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
    benzkji almost 10 years
    as 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
    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 the XMLHttpRequest.abort() method which is supposed to terminate the connection. As the XMLHttpRequeset 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
    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
    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
    Ryan Q over 8 years
    Thanks 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.