Tuesday, June 24, 2008

Rails: Exception handling has never been easier

Consider the typical case, where you've just finished writing your Rails application. You've considered all runtime errors that might appear in some specific known scenarios. You handled them all successfully, and you're about to deploy your application. The problem is, despite handling all "expected" thrown exceptions, there are still some typical exceptions that still might appear due to user input, rather than a bug in your logic.

A famous example of those exceptions is ActiveRecord::RecordNotFound. This exception is raised when calling 'find' on a model class using a non-existing id. In many cases, the model record id could be supplied as a parameter from the user. This means that such an exception, if not handled, could always be thrown, displaying an ugly stack trace to the user. Another example is ActionController::RoutingError. If a user tried a url that doesn't correspond to any application route, An ugly page will appear with a message "No route matching....". Of course, our concern also includes application bugs that might generate some exceptions. We want them to be logged and handled gracefully until we deal with them.

As expected, Rails provides an awesome generic mechanism for handling all unhandled exceptions. What's beautiful about it is that it can be added as a new aspect to the application, after we're done coding. The magical key is 'rescue_from', one of ActionController's class methods. Using rescue_from in ApplicationController, we declare a last line of defense for all unhandled exceptions in deeper levels. The following code snippet is a typical example of handling all unhandled exceptions using rescue_from:
rescue_from Exception, :with => :rescue_all_exceptions if RAILS_ENV == 'production'

def rescue_all_exceptions(exception)
case exception
when ActiveRecord::RecordNotFound
render :text => "The requested resource was not found", :status => :not_found
when ActionController::RoutingError, ActionController::UnknownController, ActionController::UnknownAction
render :text => "Invalid request", :status => :not_found
else
EXCEPTION_LOGGER.error( "\nWhile processing a #{request.method} request on #{request.path}\n
parameters: #{request.parameters.inpect}\n
#{exception.message}\n#{exception.clean_backtrace.join( "\n" )}\n\n" )
render :text => "An internal error occurred. Sorry for inconvenience", :status => :internal_server_error
end
end


Now, what exactly does this code chunk do? It's quite simple. We just declare that we want to handle exceptions of type 'Exception' (parent of all exceptions) using the method 'rescue_all_exceptions'. We add a condition to do this only if the application is running in production environment (of course we need the exception stacktrace in development rather than a clean error apology). Then we define the implementation of our exception handler 'rescue_all_exceptions'. Basically, we act according to the type of the passed exception. If it's RecordNotFound, we display a clean "not found" message. If it's a routing problem, we display a clean "invalid request" message. If it's neither of the mentioned, we assume that the exception is generated due to a bug. We log the details of the request and the exception, and display a clean apology for an internal server error.

What have we just done? We have transferred our application from an unsafe state where any runtime error could generate an ugly user-irrelevant page, to a safe state where any runtime error generates an appropriate message, logging its details for future analysis and fixing.


5 comments:

Aslan said...

this is great but what about unhanded exceptions in models or daemons?

Haitham Mohammad said...

@Aslan: This should do it for all unhandled exceptions at all levels, as long as the exception thrower is called from the controller level..

otherwise (for background processes, rakes and other stuff) you should enclose your task in a rescue block, and log it in an exceptions logger

Chip Castle said...

I running Rails 2.3.2 and found that I had to replace EXCEPTION_LOGGER.error with logger.error, plus "inspect" had a typo. Other than that, this really helped clean up my existing exception handling.

Thanks for your great example code!

Shannon -jj Behrens said...
This comment has been removed by the author.
Shannon -jj Behrens said...

I got dynamic 404s, authlogic, Cucumber, and rescue_from to all play nicely together. It was extremely rough. I wrote about it here:
http://jjinux.blogspot.com/2009/08/rails-dynamic-404s-authlogic-cucumber.html