07 Apr 2008 Handling AJAX Errors using Prototype & Rails
Ruby on Rails comes with a lot of nice helper methods for generating the JavaScript driving the AJAX calls to your controllers. Handling the responses from the HTTP server becomes a snap too, with Rails providing a few simple callbacks to handle the response from the server:
But what if the server doesn’t get a chance to respond at all? What if the user’s browser has been unexpectedly disconnected from the Internet? What if your server – god forbid – has crashed?
Most AJAX applications in the wild will simply sit there forever waiting for a response. Certainly, that’s what happened to us when we first tested such a scenario. This is unacceptable: if the browser is unable to communicate with the server, we need to let the user know somehow.
Internally, Rails uses a cross-platform Javascript library called Prototype to do the heavy lifting when making AJAX calls. When you call a helper method like link_to_remote, Rails generates code to instantiate a Prototype Ajax.Request object (or a Ajax.Updater object in some cases) to make the remote call when a certain event occurs. In the case of link_to_remote, this will be when a user clicks on the generated link.
Many of the helper methods accept a common set of options which provide callbacks for handling success or failure using the following callbacks:
- :success will be executed if the server responds to the AJAX request with any sort of 200 HTTP OK code.
- :failure will be executed if the server responds to the AJAX request with anything *but* a 200 code.
- :complete will be executed after the AJAX request has finished, irrespective of the result.
These callbacks can be mapped to corresponding callbacks in Prototype on a one-to-one basis. For the above callbacks, the corresponding Prototype callbacks are called onSuccess, onFailure and onComplete, respectively. Unfortunately, only the “complete” callback will be triggered if some fatal error occurs when communicating the server – “failure” will only be triggered if we get a response back from the remote host indicating a server-side. We have no way of determining what will happen if the browser is unable to communicate with the server at all.
So how do we detect server outage? Well it just turns out that if your browser is unable to communicate with the server, an exception will be thrown. We can actually use this as a basis for detecting server outages and other classes of errors that might occur browser-side. For this purpose, Prototype provides onException which is triggered by your browser if any sort of exception is thrown while the AJAX call is in progress. If we can tap into this callback, we can ensure our AJAX methods will dutifully report any problems that might occur.
Unfortunately Rails doesn’t yet provide access to this callback out-of-the-box, so there is no “:exception” callback available to the Rails helpers. We don’t want to clutter our templates with ugly JavaScript: we’d prefer to keep with the existing Rails conventions if we can. So what can we do?
PrototypeHelper#build_callbacks is used by remote_function to generate the Javascript responsible for making an AJAX call in helper methods like link_to_remote, form_remote_tag and others. It turns out that this little method exposes virtually all the callbacks available to Prototype *but* onException.
This is the hook we’re looking for: here we override and adapt the original code in PrototypeHelper#build_callbacks to add the following method to our ApplicationHelper:
include ActionView::Helpers::PrototypeHelper
# ...
def build_callbacks(options)
callbacks = {}
options.each do |callback, code|
name = 'on' + callback.to_s.capitalize
if CALLBACKS.include?(callback)
callbacks[name] = "function(request){#{code}}"
elsif callback == :exception
callbacks[name] = "function(request,exception){#{code}}"
end
end
callbacks
end
This makes the :exception callback available to Rails using the familiar notation seen for the other types of callback. :exception is somewhat different in that it provides the exception object thrown internally available to your callbacks as the “exception” variable. Outside of that, it’s just like any other callback.
With this new callback available to us, we can then override the AJAX helper methods to include our error handling code in ApplicationHelper. As an example, here’s how we’d do it for form_remote_tag:
alias old_form_remote_tag form_remote_tag
def form_remote_tag(options={}, &block)
# In the event of a network failure (or some other error), we want to display an error message
options[:exception] = (options[:exception] || '') +
'alert("Anerror occured while communicating with the server. Please try again later.");' +
# Bubble the javascript exception up to prevent the caller from continuing execution (failure
# to do this may result in multiple error messages being displayed!).
'throw exception;'
old_form_remote_tag(options, &block)
end
end
With this code in place, any attempts to submit a form via AJAX will include our special error detection code, so we can rest at ease knowing our users will be notified if something goes horribly wrong at the network or language leve.
When using this technique to check, you have to be quite careful in your JavaScript code. Where any language- or browser-level exception that was thrown during the AJAX request was once ignored, it will be caught by the :exception callback. This means syntax errors in the response Javascript, bad content types (e.g. a content type of “text/javascript” with a body of HTML) or other errors will trigger the :exception callback. Unfortunately there doesn’t seem to be an obvious way to get more information about the nature of the error. For our purposes, however, this seemed to be enough if we were careful with the AJAX responses.
A further improvement to this approach might use a timeout mechanism to provide a hard limit for AJAX requests: if the server doesn’t respond with data within X seconds, abort the request and display an error. This would remove the reliance on the browser reporting errors via an exception, resulting in somewhat more robust code – the feedback may not be as immediate as if one were using exceptions, however.
Don’t leave your users out in the dark: keep them informed if things go wrong with an AJAX request.
Anonymous
Posted at 07:28h, 10 JuneAnisha Hohney
Posted at 10:56h, 12 Maysuperb, fresh, issue. the do the job you put in to supply this kind of helpful