jQuery gotcha: error callback triggered on xhr.abort()

If you use jQuery’s ajax function, and use $request.abort() to cancel requests, be aware that abort() triggers the request’s error callback [renamed fail in v1.8]. This is documented, but in a roundabout way, and to me it was not obvious (intuitively, a manual call to abort() is not an error).

A tricky bug can sneak in if your ajax error handler initiates the request again. For example, say you have a long asynchronous ajax call, which you expect could be broken at any time by loss of network connectivity (e.g., in a mobile webapp). The obvious thing to do is to restart the connection:

var data_request;
function get_data_from_server() {
    data_request = $.ajax("/get_data", {
                error: function () {
                    get_data_from_server();  // Try request again.
                }
            });
 }

The above works fine. But say later on you want to abort the request:

function cancel_get_data() {
    if (data_request) {
        data_request.abort();
        data_request = null;
    }
}

Oops, that doesn’t work. The abort triggers the error handler, which calls get_data_from_server(). The request was aborted and then immediately restarted. As a nice side effect, the line data_request=null; will run after the ajax request is re-initiated, so you’ll lose the handle to the request.

The fix is simple:

       // in the ajax() call...
                error: function (xhr, text_status, error_thrown) {
                    if (text_status != "abort") {
                        get_data_from_server();  // Try request again.
                    }
                }

Now the fetch is only re-initiated when a true error occurs, and not after a manual abort().

If you want to see abort() -> error in action, here is a standalone example. It requires Tornado.

import tornado.ioloop
import tornado.web
html = """
<html>
<head>
<script src="http://code.jquery.com/jquery-1.7.2.min.js"
type="text/javascript"></script>
<script>
var req = $.ajax({url: "/never_returns",
success: function(data) { console.log("Success"); },
error: function(xhr, text_status, error_thrown) {
console.log("Error: " + text_status); }
});
function stop() {
req.abort();
}
</script>
</head>
<body>
Fetching... (will never return)
<br><br>
<button onclick="stop()">Stop</button>
</body>
</html> """
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.write(html)
class NeverReturnsHandler(tornado.web.RequestHandler):
@tornado.web.asynchronous
def get(self):
pass
application = tornado.web.Application(
[(r"/", MainHandler),
(r"/never_returns", NeverReturnsHandler)])
application.listen(8001)
tornado.ioloop.IOLoop.instance().start()

Comments are closed.