Custom dynamic error pages in Ruby on Rails
So you have finished building your Rails application, done some polishing and suddenly you notice that something went horribly wrong in production and you get the well-known default Rails error page in your browser. You fix whatever caused it and realize that damn, those pages aren’t all that good looking. You want the pages to be dynamic and use your site layout?
It is actually pretty easy to achieve that goal, you can even tailor your error pages to match whatever specific exceptions you want to.
First things first
By default, Rails displays the error pages only in production mode. In development mode you get the all-so-informational exception descriptions and stacktraces.
This behavior boils down to Rails either considering your request local (and displaying the full debug message) or “remote” (which means the application is probably in production and/or the debug messages should not be displayed). You can control this by changing your environment specific configuration.
#defaults: true for development, false for production
config.action_controller.consider_all_requests_local = false
By changing this value to false in your development environment configuration you will be able to see the custom error pages you will soon create.
On to the more fun stuff
So now that we are all familiar when and why error pages are displayed, lets make our application show more personalized and custom errors.
As i mentioned before, there is a way to define a custom handler for whatever type of Exception
your application might throw.
This is done via registering exception handlers in the controller with rescue_from
module Rescuable
module ClassMethods
...
# Rescue exceptions raised in controller actions.
def rescue_from(*klasses, &block)
...
end
...
end
end
end
With this method you can register a method to handle one or multiple exceptions or create the handler inline as a Proc (notice how the method accepts a variable amount of “klasses” and a block as a last parameter).
Make it work!
In this example I want to have a custom 404 page for any routing errors, unknown controller and/or action errors and in any cases where ActiveRecord
might throw a RecordNotFound
exception.
Some might argue that the last error should not happen at all, but in case of for example social networking, its easy to crawl URLs and type in invalid user IDs.
For every other exception I just want to display a custom 500 page.
...
unless ActionController::Base.consider_all_requests_local
rescue_from Exception, :with => :render_error
rescue_from ActiveRecord::RecordNotFound, :with => :render_not_found
rescue_from ActionController::RoutingError, :with => :render_not_found
rescue_from ActionController::UnknownController, :with => :render_not_found
rescue_from ActionController::UnknownAction, :with => :render_not_found
end
...
end
As the exception handlers list seems to be LIFO, I need to register the top level exception handler first followed by the specific exceptions. This ensures that the handlers get traversed in the right order and specific exceptions will be caught at the right handler.
unless ActionController::Base.consider_all_requests_local
ensures that these custom error handlers will only be run while config.action_controller.consider_all_requests_local
is set to false.
And the exception handlers look like this:
...
private
def render_not_found(exception)
log_error(exception)
render :template => "/error/404.html.erb", :status => 404
end
def render_error(exception)
log_error(exception)
render :template => "/error/500.html.erb", :status => 500
end
...
end
Notice the log_error(exception)
call in there: this will ensure that your exceptions will also be properly logged.
Without this, you will rescue from all the exceptions with a nice error page and know nothing about them even occuring even in the logs.
But but… im using Hoptoad too!
As Hoptoad uses alias_method_chain
to hook into rescue_action_in_public
, which is called after rescue_from
, you exceptions will never end getting notified to Hoptoad.
To avoid this from happening, you simply need to notify Hoptoad yourself in your exception handlers.
In our example, it only makes sense to report everything caught in the general exception handlers as any notifying about any routing errors does not make much sense (it would probably get real spammy real fast).
log_error(exception)
notify_hoptoad(exception)
render :template => "/error/500.html.erb", :status => 500
end
It’s really that easy, you don’t need to do anything else.
To try out your fresh straight-from-the-over exception handler, change config.action_controller.consider_all_requests_local
to false in your development configuration and give it a try.
Wait, there is more
There are times when you invoke some ActiveRecord
queries from your views (yes, its okay if you know what you are doing). For example you call a helper method, which in turn queries the database.
This query cannot find the record you asked for and raises a RecordNotFound
exception. And oddly enough your rescue_from
handlers will not catch it. Instead you will receive a general exception page.
This will actually happen to any exceptions raised in the view and is caused by Rails wrapping these exceptions in its own TemplateError
.
In my case, I couldn’t find any sensible way to bypass this behavior (of rescue_from
not catching the correct exception from inside the TemplateError
) so i just went on to mokey-patch the ActionView::Template
class
module ActionView
class Template
def render_template(view, local_assigns = {})
render(view, local_assigns)
rescue Exception => e
raise e unless filename
case e
when ActiveRecord::RecordNotFound
raise e
when TemplateError
e.sub_template_of(self)
raise e
else
raise TemplateError.new(self, view.assigns, e)
end
end
end
end
I’m basically just filtering out the exception of interest to me and raising it directly for it to get caught by the rescue handlers defined in the controller.
Don’t forget to require this file in your initializer or configuration file:
All this should pretty much cover the basics for custom error pages and make your site look a bit more professional while handling errors.
29 Comments
Also consider just using:
render_optional_error_file
Thank you for your comment, Mischa.
It probably comes down to how much control you need or want. The render_optional_error_file resolves the exception to an error code and then invokes the handler, passing only the resolved code. All that is left is to display a page for that error code.
On the other hand, rescue_from is invoked when the exception is caught and it is passed to the handler as an argument. This leaves me in full control of the error and I can basically use it for my own debugging user interface or do some freaky custom responses or actions for my custom exceptions.
I just prefer to have more fine-grained control, but yeah by no means is this the only right way to handle errors :)
Great write up, very detailed and informative. I wanted to mention if you’re using authlogic you need to add:
activate_authlogic
to your method.
So the whole thing would look like:
log_error(exception)
notify_hoptoad(exception)
activate_authlogic
render :template => "/error/500.html.erb", :status => 500
end
I think you mean
“By changing this value to *******FALSE****** in your development environment configuration you will be able to see the custom error pages you will soon create.”
No?
Yes, you are entirely correct, should be correct now.
Thank you for pointing this out.
Having a dynamic error page is nice, but there’s always the risk of generating another exception and entering a loop until the stack overflows, correct?
That is correct: it will happen if you are careless. Thats why you should always rescue from specific errors and have a safe fallback for the most generic one, a fallback that is as dynamic as possible – to avoid falling into an endless loop :)
Thank you for this informative post.
How does one invoke the default exception render?. I’d like to conditionally show a custom exception page.
]{
One quibble: it’s never OK to query the DB from your views. Helper methods should not touch the DB. If you think you need that, then what you actually need is render_component or Cells.
Great article. You may want to rescue from StandardError instead of Exception — Exceptions are for more “system” level stuff which is rare and serious enough that you maybe want it to keep trickling up.
That said, if you are reporting the exception with hoptoad/exceptional anyway… I suppose it could be best for the user experience to catch everything. There might be some security implications though, not sure.
Aitäh selle posti eest! Väga hästi töötab :)
Nice :)
def render_error(exception)
render :text => “#{exception.inspect}#{exception.backtrace.join(“”)}”
end
Thanks for the write-up. I’m trying this out and it appears that “log_error” is a custom method that you’ve added to your controllers. Is that correct?
I have the same problem. log_error is a custom method
I’m seeing the same thing, looks like log_error is undefined in Rails 3.2-beta. Was this implemented in an earlier Rails version, or is it a custom method? Is there something else I should replace it with in this version of Rails?
Just do logger.error instead, I think log_error was depreciated.
Hey everybody! ..
i would like to know, why continuously –
Customdynamic.error Problem
appears, when i would pay with pay pal ?
Rails detects when you are browsing with ip 127.0.0.1 and shows you the development environment errors even if you are in production environment. You should try accessing from a different machine to get the proper errors.
For rails > 3 : http://stackoverflow.com/a/7139803/75246
Hi, great post, but is there any way of catching low level exceptions like suppose we lost database connectivity(it happens to me often cuz my development database servers are at remote location), would love to listen about this exception.
thanks
This is great thanks, but I can’t get the ActiveRecord 404 errors that you mention to catch using this method, even with the “monkey patch”…
Using Rails 3.2
Thank you for the great post! But “ActionController::Base.consider_all_requests_local” has been deprecated. Use “config.consider_all_requests_local” instead.
With Rails 3.1 at least, it seems it should be
config.consider_all_requests_local
instead of
ActionController::Base.consider_all_requests_local
Well actually,
::Rails.application.config.consider_all_requests_local
For RoutingError, this will no longer work in Rails 3.2.
Here’s a simple way to handle RoutingError:
http://robert-reiz.com/2012/05/01/handling-routing-error-in-rails-3-2/
You can use that technique to route to application#render_not_found
If you want to still see the error message in development, add this to you render_not_found method:
if Rails.application.config.consider_all_requests_local
raise ActionController::RoutingError.new(‘Not Found’)
end