41

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.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Can you elaborate more? From what you said it looks like a bad design – zerkms Jul 17 '12 at 21:34
  • A deferred either passes or fails. You can't cancel it or remove the handlers. Maybe there is a better way to do what you are trying to do. – Kevin B Jul 17 '12 at 21:35
  • 1
    Your `.fail(jqXHR, textStatus, errorThrown)` handlers could just check whether `textStatus` is "abort". – quietmint Mar 11 '14 at 15:17

4 Answers4

17

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.

jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • 3
    I'd better go with `rejectWith`, but +1 for the idea – zerkms Jul 17 '12 at 22:01
  • @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. – jfriend00 Jul 17 '12 at 22:03
  • 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 ;-) – zerkms Jul 17 '12 at 22:03
  • @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? – jfriend00 Jul 17 '12 at 22:04
  • 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 Jul 17 '12 at 22:04
  • I would just check if `{ manuallyAborted: true }` passed to the `fail` handler – zerkms Jul 17 '12 at 22:05
  • @zerkms - OK, you made different assumptions than I did. The OP didn't say anything about doing an `abort()`. – jfriend00 Jul 17 '12 at 22:05
  • "I need to cancel the previous call to make sure that it doesn't return after the second and show old data". – zerkms Jul 17 '12 at 22:06
  • @zerkms - My proposal cancelled it's effect by ignoring the result - different interpretation. The OP can choose which way they want to go. – jfriend00 Jul 17 '12 at 22:25
  • @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! – benzkji May 22 '14 at 12:18
  • @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. – zerkms May 22 '14 at 20:10
  • @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. – jfriend00 May 22 '14 at 21:41
  • as Erv Walter states in his comment on the accepted answer: http://stackoverflow.com/questions/446594/abort-ajax-requests-using-jquery (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. – benzkji May 23 '14 at 21:16
  • @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. – jfriend00 May 23 '14 at 22:04
  • @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. – benzkji May 24 '14 at 07:05
  • @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. – jfriend00 May 24 '14 at 07:18
7

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

Ryan Q
  • 10,273
  • 2
  • 34
  • 39
  • 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. – ilovett Dec 13 '13 at 20:04
  • 3
    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. – Ryan Q Nov 17 '15 at 21:00
1

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.

Dtipson
  • 1,564
  • 16
  • 21
1

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();
Joshua Hansen
  • 495
  • 4
  • 4