March 13th, 2013 by Josh

Dynamic Error Pages In Rails

It's a little known fact that you can easily substitute the default Rails HTML error pages with something more pleasant. You may have noticed the 404.html, 422.html and 500.html files that are generated with every new Rails project and wondered if there's a clean way to style them like the rest of your application. There is, and it's surprisingly simple.

Basic Implementation

The default status code templates are served by a Rack exception application. You can override this to be any Rack compatible app, including your applications router:

# config/application.rb
config.exceptions_app = self.routes

This will route any exceptions caught to your router Rack app. Now you'll want to define routes to display those errors yourself:

# config/routes.rb
get "/404", :to => "errors#not_found"
get "/422", :to => "errors#unacceptable"
get "/500", :to => "errors#internal_error"

This will route each error code to it's respective action in ErrorsController. Now we'll want to define those actions:

class ErrorsController < ApplicationController

  def not_found
    render :status => 404
  end

  def unacceptable
    render :status => 422
  end

  def internal_error
    render :status => 500
  end

end

We tell each action to render the appropriate HTTP status code related to the error that's been caught. All that's left to do now is create the view related to each action and you're done:

# app/views/errors/not_found.html.haml
%h1 404 - Not Found

When we visit /404 our 404 - Not Found view should render as expected. Now you can style your error pages without having to duplicate any styles into the public directory of your application.

Optimising Our Errors Controller

So far we've got working error pages, but it doesn't feel like the most DRY implementation. We could make it more RESTful by refactoring our errors controller to use a show action instead.

Let's start by changing our routes:

# config/routes.rb
%w( 404 422 500 ).each do |code|
  get code, :to => "errors#show", :code => code
end

Now we need to ensure our ErrorsController uses the code parameter we're passing through:

class ErrorsController < ApplicationController

  def show
    render status_code.to_s, :status => status_code
  end

protected

  def status_code
    params[:code] || 500
  end

end

To clean things up even more I've created a status_code method which defaults to a 500, this will protect any cases where we might not have the code present in the params hash.

The final alteration as part of this refactor is to rename our view files to use status codes rather than our previous naming scheme:

# app/views/errors/404.html.haml
%h1 404 - Not Found

And that's it. We've now got a reusable errors controller which is flexible enough for us to add new error types to in the future (by adding a new code to the error codes array and a corresponding view file).

It's worth noting that you shouldn't be doing anything fancy in these views. The reason these pages are rendered is because something has most likely gone wrong in your application, so you should probably stray away from making calls to the database or performing more complex actions.