Managing Ajax Errors

by David Joly | Wednesday, May 5th, 2010

Ajax is an incredibly powerful asset in the world of web development. Developers can create web applications that no longer need to load a new page for every request. This can make using the web application faster and more enjoyable. The only caveat is, when using Ajax you have to have some way to be sure your request completes successfully, and a fallback if it should fail.

Say Hello to jQuery ajax()

After spending months using jQuery’s get(), post(), and load() functions to make Ajax requests, I discovered a better way. The aforementioned functions are mere wrappers for jQuery’s ajax() function which is, as you might expect, far more useful. jQuery’s post() and get() functions don’t give you a means to handle errors and make you rely on postback data to manage activity indicators. Ajax() has no such limitations.

The following tutorial demonstrates a simple implementation of ajax() to manage errors and more easily return the DOM to the desired state when the XHR request ends. I am using the Zend Framework for the server-side scripts in this tutorial. Visit http://framework.zend.com/manual/en/learning.quickstart.intro.html for a tutorial on setting up a simple Zend Framework application.

Provide for the Common Error

The following function is an ideal candidate to be included in a header script so it can be used for all Ajax requests. The function will be passed the exact same XHR and error status data that is available to us through the ajax() function.

//Here we set up our error handler function.
    function ajaxErrorHandler(xhr,status,error)
    {
		switch(status){
			case 'error':
			switch(xhr.status){
				case 401:
					alert('HTTP ' + xhr.status + 'Error\n'+xhr.responseText);
					break;
				case 404:
					alert('HTTP ' + xhr.status + 'Error\n'+xhr.responseText);
					break;
				case 500:
					alert('HTTP ' + xhr.status + 'Internal Server Error');
					break;
				default:
					alert('HTTP ' + xhr.status + 'Error\n'+xhr.responseText);
				break;
			}
			break;
			case 'timeout':
				alert('Timeout. The server was taking too long to respond. Please try again in a few moments.');
				break;
			case 'parsererror':
				alert('Error.\nCould Not Process Server Response.');
				break;
			default:
				alert('Unexpected HTTP Error.\n'+xhr.responseText);
			break;
		}
	}

We also specify how long before the request times out and a timeout error occurs. This can be set in the ajax() function for each ajax request if need be, but it makes sense to set this globally.

//Here we tell the browser how long to wait on the server before timing out.
    $.ajaxSetup({timeout:20000});

Creating the Ajax Triggers

Now let’s set up a few buttons to help demonstrate Ajax error handling.

<p>
	<button id="not_found">Not Found</button>
	<span class="status"></span>
</p>
<p>
	<button id="access_denied">Access Denied</button>
	<span class="status"></span>
</p>
<p>
	<button id="timeout">Timeout</button>
	<span class="status"></span>
</p>
<p>
	<button id="success">Success</button>
	<span class="status"></span>
</p>

Create the Backend Script

In this demonstration I am using the Zend Framework. You can substitute with your own scripts if you like, using just basic PHP, C#, Java, or whatever else floats your boat.
Notice how my notfound and accessdenied scripts set the response headers to 404 and 401, respectively. These are detected client-side by the ajaxErrorHandler() function.

public function successAction()
    {
    	$this->_helper->getHelper('layout')->disableLayout();
    	$this->_helper->viewRenderer->setNoRender();
    	$response = new stdClass;
    	$response->code = 1;
    	$response->message = 'This message is from the server!';
    	echo json_encode($response);
    }
 
    /*
     * This action is simulating a busy server...
     */
    public function timeoutAction()
    {
    	while(true){}
    }
 
    /*
     * If a user is denied access to the requested resource,
     * the user should be informed. In a real project, the current
     * user's permissions would be checked (using Zend_Acl for example)
     * to verify if they have permission to access the resource, and
     * forward them to an access denied message if denied.
     *  
     */
	public function accessdeniedAction()
    {
    	//Set response code (very important)
    	$this->getResponse()->setHttpResponseCode(401);
    	if($this->getRequest()->isXmlHttpRequest()){
    		//Disable layout and view if xhr request is detected.
    		$this->_helper->getHelper('layout')->disableLayout();
    		$this->_helper->viewRenderer->setNoRender();
    		echo 'Failed to connect to restricted resource.';
    	}
    }
 
    /*
     * If for some reason a resource does not exist
     * on the server, the server should return a not found
     * message to the user.
     */
    public function notfoundAction()
    {
    	//Set response code (very important)
    	$this->getResponse()->setHttpResponseCode(404);
    	if($this->getRequest()->isXmlHttpRequest()){
    		//Disable layout and view if xhr request is detected.
    		$this->_helper->getHelper('layout')->disableLayout();
    		$this->_helper->viewRenderer->setNoRender();
    		echo 'Resource Not Found';
    	}
    }

Activate the Ajax Triggers

Each Ajax trigger receives its own ajax() goodness. To let the user know Ajax activity is taking place, I have jQuery appending an Ajax indicator gif image and setting a status message associated with that trigger.

There are two options you can pass to the ajax() function you should know. Those are the error and context options. The error option allows us to specify a function to execute should an error occur. The context option lets us specify a DOM object we want to have available to us in the error and success callback functions. The DOM object can be accessed by the error and success functions using “$(this)”, making it a cinch to remove our trigger’s activity indicator and change its status message. This comes in really handy when you have repeated Ajax controls.

$('#not_found').click(function(){
	//Ajax activty triggered, inform user
	$(this).attr('disabled');
	$(this).after('<img class="activity-indicator" src="images/ajax-activity.gif"/>');
	$(this).siblings('.status').text('Requesting a non-existent resource...');
 
	$.ajax({
		//Setting context to 'this' enables us to
		//to use 'this' to access the ajax trigger 
		context:$(this),
		url:'/may042010/notfound',
		data:{},
		type:'post',
		dataType:'json',
		success:function(data,status,xhr){},
		error:function(xhr,status,error){
			//Handle Error
			ajaxErrorHandler(xhr,status,error);
			//Remove activity indicator
			$(this).siblings('.activity-indicator').remove();
			//Reset status message
			$(this).siblings('.status').text('I could not find it!');
		}
	});
});
 
$('#access_denied').click(function(){
	//Ajax activty triggered, inform user
	$(this).attr('disabled');
	$(this).after('<img class="activity-indicator" src="images/ajax-activity.gif"/>');
	$(this).siblings('.status').text('Requesting a restricted resource...');
 
	$.ajax({
		//Setting context to 'this' enables us to
		//to use 'this' to access the ajax trigger 
		context:$(this),
		url:'/may042010/accessdenied',
		data:{},
		type:'post',
		dataType:'json',
		success:function(data,status,xhr){},
		error:function(xhr,status,error){
			//Handle Error
			ajaxErrorHandler(xhr,status,error);
			//Remove activity indicator
			$(this).siblings('.activity-indicator').remove();
			//Reset status message
			$(this).siblings('.status').text('I was denied!');
		}
	});
});
 
$('#timeout').click(function(){
	//Ajax activty triggered, inform user
	$(this).attr('disabled');
	$(this).after('<img class="activity-indicator" src="images/ajax-activity.gif"/>');
	$(this).siblings('.status').text('Request set to a very busy server...');
 
	$.ajax({
		//Setting context to 'this' enables us to
		//to use 'this' to access the ajax trigger 
		context:$(this),
		url:'/may042010/timeout',
		data:{},
		type:'post',
		dataType:'json',
		success:function(data,status,xhr){},
		error:function(xhr,status,error){
			//Handle Error
			ajaxErrorHandler(xhr,status,error);
			//Remove activity indicator
			$(this).siblings('.activity-indicator').remove();
			//Reset status message
			$(this).siblings('.status').text('The server must be sleeping!');
		}
	});
});
 
$('#success').click(function(){
	//Ajax activty triggered, inform user
	$(this).attr('disabled');
	$(this).after('<img class="activity-indicator" src="images/ajax-activity.gif"/>');
	$(this).siblings('.status').text('Request sent to a not-so busy server...');
 
	$.ajax({
		//Setting context to 'this' enables us to
		//to use 'this' to access the ajax trigger 
		context:$(this),
		url:'/may042010/success',
		data:{},
		type:'post',
		dataType:'json',
		success:function(data,status,xhr){
			if(data.code == 1){
				//Reset status message
				$(this).siblings('.status').text(data.message);
			}
			//Remove activity indicator
			$(this).siblings('.activity-indicator').remove();
		},
		error:function(xhr,status,error){
			//Handle Error
			ajaxErrorHandler(xhr,status,error);
			//Remove activity indicator
			$(this).siblings('.activity-indicator').remove();
			//Reset status message
			$(this).siblings('.status').text('');
		}
	});
});

You can download the files used in this tutorial here. Documentation on jQuery’s ajax() function can be found by going to http://api.jquery.com/jQuery.ajax/.

Comment on this Article

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="">

Posting Code: You can post code using the <pre> tag. Specify the language in the lang attribute, like this: <pre lang="php"> //Code Goes Here </pre>