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.

# Show full error reports and disable caching
#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 ActiveSupport
  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.

class ApplicationController < ActionController::Base
  ...
  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:

class ApplicationController < ActionController::Base
  ...
  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).

def render_error(exception)
  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

# /lib/ext/action_view/template.rb
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:

require 'ext/action_view/template'

All this should pretty much cover the basics for custom error pages and make your site look a bit more professional while handling errors.

Tanel Suurhans
Tanel is an experienced Software Engineer with strong background in variety of technologies. He is extremely passionate about creating high quality software and constantly explores new technologies.

29 Comments

  • Mischa

    Also consider just using:

    render_optional_error_file

    • Tanel Suurhans

      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 :)

  • Richard Schneeman

    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:

     def render_error(exception)
     log_error(exception)
     notify_hoptoad(exception)
     activate_authlogic
     render :template =&gt; "/error/500.html.erb", :status =&gt; 500
     end
  • Erik

    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?

    • Tanel Suurhans

      Yes, you are entirely correct, should be correct now.
      Thank you for pointing this out.

  • Adriano

    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?

    • Tanel Suurhans

      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 :)

  • Kristoph

    Thank you for this informative post.

    How does one invoke the default exception render?. I’d like to conditionally show a custom exception page.

    ]{

  • Marnen Laibow-Koser

    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.

  • John

    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.

  • teeniimoodi

    Aitäh selle posti eest! Väga hästi töötab :)

  • Lucas Catón

    Nice :)

  • grosser

    def render_error(exception)
    render :text => “#{exception.inspect}#{exception.backtrace.join(“”)}”
    end

  • Jerod Santo

    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?

    • JEremy

      I have the same problem. log_error is a custom method

    • Matt Huggins

      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?

  • Ali

    Just do logger.error instead, I think log_error was depreciated.

  • Jennifer

    Hey everybody! ..
    i would like to know, why continuously -

    Customdynamic.error Problem

    appears, when i would pay with pay pal ?

  • Agus @ ROR Developer

    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.

  • Nikos Dimitrakopoulos

    For rails > 3 : http://stackoverflow.com/a/7139803/75246

  • Ismail

    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

  • Chris Edwards

    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

  • Dano

    Thank you for the great post! But “ActionController::Base.consider_all_requests_local” has been deprecated. Use “config.consider_all_requests_local” instead.

  • Sagie

    With Rails 3.1 at least, it seems it should be
    config.consider_all_requests_local
    instead of
    ActionController::Base.consider_all_requests_local

  • Sagie

    Well actually,
    ::Rails.application.config.consider_all_requests_local

  • Dan

    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

Liked this post?

There’s more where that came from. Follow us on Facebook, Twitter or subscribe to our RSS feed to get all the latest posts immediately.