Defensive AJAX and AJAX retries in jQuery

A lot of code that I have seen over the years always assumes success, particularly with AJAX calls. This creates code that is fragile, and entirely dependant on the result of external (to the client) code.

There are a few ways we can attempt to protect against this. The first, is to always depend on the result of the call to make any changes to the DOM or UI. For example, let’s say we have a quantity field in a shopping basket on an e-commerce site. When the user clicks a plus next to the field, we make an AJAX call that increments a value in a basket stored server-side and updates the UI to match. It is very common, and tempting, to increment the value in the UI immediately, and then make the AJAX call. There are a few obvious problems here. If the AJAX call fails, now we have a UI that is inaccurate. We can decrement the value in the UI to make up for it, but now we are starting to confuse the user with the numbers jumping up and down, and if they have clicked the button six times in quick succession (very common use of this kind of control) our chance of error for accurate UI representation of data increases dramatically.

A much safer way to do this is to return the current stored quantity value from the server with each AJAX call, and only update the UI when the AJAX call completes. This results in a less “snappy” feeling UI, and it will be necessary to display some kind of visual cue that something is going on the background, but the user learns quickly how the site works and this is a much more robust process.

Without having any kind of genuine statistics to call upon, I would suggest that nine out of ten AJAX calls that fail are due to an issue that is temporary and would be resolved by a retry. Anything due to network issues, a lost packet somewhere, a brief server glitch, load balancing problems and so on can cause a timeout or 404 without the target resource actually being missing or consistently failing. Often when a website fails to load, I click refresh and there it is. An AJAX call is just the same. (The same applies to database connections, or anything dependant on a network resource.)

Therefore, rather than display an error on an AJAX timeout (“we could not connect to the server” or “there was a problem, please try later”) or worse, doing nothing at all, there are some things we can try to resolve the issue ourselves without bothering the user about it until we’re certain that it is broken.

Let’s look at a typical jQuery AJAX call.

$.ajax({
	url : 'ajaxurl.json',
	type : 'get',
	data : 	{name : 'value'},
	dataType : 'json',
	timeout : 20000,
	success : function(json) {
		//do something
	}
});

The immediate problem with this is it completely assumes (and depends upon) success. There is not even a basic error handler. Something like this is better:

$.ajax({
	url : 'ajaxurl.json',
	type : 'get',
	data : 	{name : 'value'},
	dataType : 'json',
	timeout : 20000,
	success : function(json) {
		//do something
	},
	error : function() {
		alert('Oops! There was a problem, sorry.');
	}
});

The error callback function is actually passed three arguments.

  • The XMLHTTPRequest object in use
  • A textual equivalent of the status
  • The actual exception thrown

These allow us to react in a more sophisticated manner:

$.ajax({
	url : 'ajaxurl.json',
	type : 'get',
	data : 	{name : 'value'},
	dataType : 'json',
	timeout : 20000,
	success : function(json) {
		//do something
	},
	error : function(xhr, textStatus, errorThrown ) {
		if (xhr.status == 500) {
			alert('Oops! There seems to be a server problem, please try again later.');
		} else {
			alert('Oops! There was a problem, sorry.');
		}
	}
});

Since the error function lives inside the AJAX object itself, and is called in that context, the this keyword very usefully points to the jQuery AJAX instance itself. Using this, and the arguments we are being passed we can very easily set the UI to retry the AJAX call on our behalf. Let’s attach some extra properties to the AJAX object:

$.ajax({
	url : 'ajaxurl.json',
	type : 'get',
	data : 	{name : 'value'},
	dataType : 'json',
	timeout : 20000,
	tryCount : 0,
	retryLimit : 3,
	success : function(json) {
		//do something
	},
	error : function(xhr, textStatus, errorThrown ) {
		if (xhr.status == 500) {
			alert('Oops! There seems to be a server problem, please try again later.');
		} else {
			alert('Oops! There was a problem, sorry.');
		}
	}
});

We have added tryCount and retryLimit. These are going to store how many attempts we have made, and how many attempts we will make respectively. Making use of these:

$.ajax({
	url : 'ajaxurl.json',
	type : 'get',
	data : 	{name : 'value'},
	dataType : 'json',
	timeout : 20000,
	tryCount : 0,
	retryLimit : 3,
	success : function(json) {
		//do something
	},
	error : function(xhr, textStatus, errorThrown ) {
		if (textStatus == 'timeout') {
			this.tryCount++;
			if (this.tryCount <= this.retryLimit) {
				//try again
				$.ajax(this);
				return;
			}
			alert('We have tried ' + this.retryLimit + ' times and it is still not working. We give in. Sorry.');
			return;
		}
		if (xhr.status == 500) {
			alert('Oops! There seems to be a server problem, please try again later.');
		} else {
			alert('Oops! There was a problem, sorry.');
		}
	}
});

So, now we are trying three times before giving in. Where I have actually used this, some modal dialogue boxes are used instead of window.alert() and I save a copy of the AJAX object when we reach our retry limit. At that point, although I tell the user we have given in, I still provide them with a button to try again themselves.

I am convinced that implementing techniques like this will rid us of many unnecessary bad user experiences, and many support calls. Keep the user informed (in simple language!), and assume things will fail. I hope this is helpful.

11 thoughts on “Defensive AJAX and AJAX retries in jQuery

  1. Agree quite a bit, this is the reason why I hate to love implementing AJAX, “fragile” has to be the most accurate term for it, especially if the backend isn’t as strong as the front end implementation, as it’s quite often flakey!

    Really nice post.

  2. there seems to be a problem when using timeout in this code:
    ajaxEx: function(settings, urls) {
    var originalErrorFunc = settings.error;
    var succeeded = false;
    var result;
    var urls = (settings.url instanceof Array) ? settings.url : [settings.url];
    //settings.mode = ‘sync’;
    settings.error = function(xhr, status, error) {
    var url = urls.shift();
    //alert(“Failed: ” + settings.url + “nStatus: ” + status + “n” + error + “nNext: ” + url);
    if (url) {
    settings.url = url;
    $.ajax(settings);
    } else if (originalErrorFunc) {
    originalErrorFunc.apply(settings, arguments);
    }
    };
    settings.url = urls.shift();
    $.ajax(settings);
    }

    you would call the function like $.ajaxEx({url: []}); and a new url will be used for the next ajax call if the previous fails with an error. However, the first error is most likely “parsererror” when loading xml data. The second error reported will be timeout.

  3. This looks like a fantastic idea but for some odd reason, I’m getting a too much recursion error. Any ideas?

    $.ajax ({
    url: url,
    data: data,
    timeout: 2000,
    requestId: model.requestId,
    tryCount: 0,
    retryLimit: 4,
    success: function(data) {
    controller.log(“Request ” + this.requestId + ” success.”);
    fn_success(data);
    return;
    },
    error: function(xhr, status, ex) {
    if (status == ‘error’ && xhr.status == 404) {
    controller.log (“Request ” + this.requestId + ” failed for good.”);
    fn_error();
    return;
    }
    this.tryCount++;
    if (this.tryCount <= this.retryLimit) {
    controller.log ("Request " + this.requestId + " failed. Trying " + this.tryCount + " time.");
    controller.log(this);
    $.ajax(this);
    return;
    } else {
    controller.log ("Request " + this.tryCount)
    fn_error();
    return;
    }
    }
    });

  4. This is a great idea! However, if your site has lots of ajax calls, it’s a fairly large block to include. Is there a good way to move this onto the global ajaxError() handler ?

  5. Hi guys,

    Excellent tutorial you’ve made here, it really helped me out :)

    I’m having one problem though – regarding the timeout. When I loose my connection it fires all 3 retries in an instant. Instead of waiting the 20000 miliseconds that is defined in my Ajax obj.

    $.ajax({
    type: ‘POST’,
    url: baseURL,
    timeout: 15000,
    tryCount: 0,
    retryLimit: 5,
    success: function (data) {
    //My stuff
    },
    error: function (XMLHttpRequest, textStatus, errorThrown) {

    if (XMLHttpRequest.status == 0 || textStatus == ‘timeout’) {

    this.tryCount++;
    alert(“We’ve tried recreating your connectionRetry: (” + this.tryCount + ‘/’ + this.retryLimit + ‘)’);

    if (this.tryCount < this.retryLimit) {
    //try again
    $.ajax(this);
    return;
    }
    else {
    alert("We've tried – it's not working sorry.");
    }
    }
    else {
    alert("not working");
    }
    }
    });

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>