Rails, Ajax and Exceptions? Bring it on.
Recently I’ve been working on a content management system that’s very Ajax heavy. I think Ajax is great, but if your request raises an exception you’re going to get some unexpected results. In the case where your Ajax request is expecting a JavaScript response to evaluate the enduser will sit there with no response and no indication of an error. We can do better than that.
If your application uses Ajax in two or three places, it’s not hard to code for those specific use cases, but what about three or four instances per page? We’ll need to DRY this up.
Exceptions, exceptions, exceptions
Rails will raise an exception for many reasons. Here are a few examples from ActiveRecord. If you’re finding a record that doesn’t exist an ActiveRecord::RecordNotFound exception will be raised.
Item.find(params[:id]) # params[:id] => -1
Calling save! on an ActiveRecord model can raise ActiveRecord::RecordInvalid if any of the validations fail or ActiveRecord::RecordNotSaved if any of the model’s before filters return false. Check out the Rails API if you’d like to see all of the exceptions ActiveRecord can raise.
When an exception is raised and not caught Rails returns a page with information about the exception. That page is returned with an HTTP status code of 500, “Internal Server Error.” This status code indicates an error occurred where a successful page response would have returned with the status code of 200, “OK.”
These status codes are very important to JavaScript when we make our Ajax requests.
Go ahead JavaScript. You know what to do.
We’ve touched on HTTP status codes so lets move to JavaScript. To keep things simple I reserved the class “overlay” for any link that I wanted to respond in a modal view. We can use link_to just like we normally would, but make sure to specify the class name.
<!-- rails code --> <%= link_to 'New Item', new_item_path, :class => 'overlay' %> <!-- html output --> <a href="/item/new" class="overlay">New Item</a>
I used jQuery to tie into the click event of any link with the class “overlay.” I also used live opposed to click in jQuery to ensure this functionality works for any link whether its on the page at load time or added by some other means.
$('a.overlay').live('click',function(){ $.get(this.href, function(html){ $('body').prepend(html) }); return false; });
I also setup my items controller to respond to any request with an overlay layout instead of the default application layout. This layout is structured to be prepended to the body of the HTML document.
class ItemsController layout 'overlay' def new @item = Item.new end end
Everything so far is pretty straightforward so lets get fancy.
jQuery allows you to set defaults in $.ajaxSetup for any Ajax request made. Anything setup in $.ajaxSetup will affect $.ajax, $.post and $.get. Remember the HTTP status codes we went over in the exceptions section, a successful response is status code 200 and an exception uses the 500 code. jQuery allows you to respond differently depending on whether the response was successful or an error.
When using the shorthand methods $.get and $.post we can’t define a block of code to run if an error occurs, but thats fine because we want to set it in the $.ajaxSetup.
$.ajaxSetup({ error: function(res){ $('body').prepend(res.responseText) } });
Now that we’re catching every error response let’s define what that response should be.
Rails exceptions? Make no exceptions.
To catch one exception we use rescue and define the other block of code to run.
def new @item = Item.new rescue render 'shared/exception', :layout => 'overlay' end
That works, but we’d need to do the same thing for the create, edit, update and destroy method and thats just one controller! We can catch every exception Rails raises, but in one spot. For that we’re going to use around_filter in the ApplicationController. If you haven’t used around_filter it’s pretty simple. When a method is set as an around_filter the actual request is processed and passed into the around_filter method as a block.
class ApplicationController around_filter :xhr_exceptions # ... private def xhr_exceptions yield rescue render('shared/exception', :layout => 'overlay', :status => 500) if request.xhr? end end
Notice yield is used in the xhr_exceptions method because, like written above, the request is passed in as a block. If that yield call raises an exception, rescue will catch it and the ‘shared/exception’ will render. The if statement is there to only render ‘shared/exception’ if the request is made through Ajax. Lastly, the status => 500 triggers jQuery to process this response as an error and run the error function we setup in $.ajaxSetup.
Conclusion
Exceptions can happen. No matter how well we’ve coded our application, if the database becomes unavailable you’re going to get an exception. With the help of jQuery and around_filter, any exception raised during an Ajax request is guaranteed a response we’ve designated so the enduser can be informed of the situation. Our code is as DRY as possible and our application remains solid. I hope this helps!
Comments
Comments have been closed.