Back to Tutorials
Jutsu>Rails6/8/2025

Custom Error Pages in Rails 4+

Custom Error Pages in Rails 4+

Rails' standard error handling is pretty weak (and ugly) out of the box:

rails-error

If you want to keep your app's branding consistent, this article will explain several ways to do it.

--

How Rails Handles Exceptions

Each time you send a request to a website, you'll be presented with a response belonging to one of the following categories:

If you've been developing with the web for any time, you'll undoubtedly be aware of at least the 404 and 500 error responses. These are part of the two response classes which constitute errors - 40x and 50x. The rest demonstrate successful requests.

You can read about the different error types on Wikipedia.

All web applications have to support these error messages. They are, after all, responses to requests. You can get good responses (10x / 20x / 30x) and erroneous responses (40x / 50x). The key for us is to make sure we catch the erroneous responses and direct them to our own error pages.

The way Rails handles errors is through a middleware hook called exceptions_app. This captures any exceptions coming from Rails, allowing the middleware to create an appropriate response. In short, it's responsible for keeping your web server online if a problem does occur (as opposed to making it crash):

This middleware [exceptions.app] rescues any exception returned by the application # and calls an exceptions app that will wrap it in a format for the end user.

All the tutorials you find on how to create custom error pages hook into exceptions_app, allowing you to "divert" the user in the way you want. The "quality" of your error handling is dependent on what you do after you've caught the error. This is what we are to discuss:

--

Catching errors with exceptions_app

exceptions_app is a middleware hook for the ActiveDispatch::ShowExceptions middleware:

enter image description here

Thus, any time you want to interject your own logic to the exception handling process, you have to override the exception_app middleware hook; pointing it to your own exception handling infrastructure.

There are two ways to do this:

  1. Send the exception to the routes (which then refers onto a specific controller/action)
  2. Invoke a controller directly

Let's explore both methods (they're very similar):


1. Routes

If you want to have basic error response pages, you'll be best sending to your application's routes.

This is the most common form of error handling, as it does not require a lot of customization:

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

#config/routes.rb
if Rails.env.production?
    get '404', to: 'application#page_not_found'
    get '422', to: 'application#server_error'
    get '500', to:  'application#server_error'
end

This gives you the ability to create the following methods to your controller (in this case application):

#app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
    after_filter :return_errors, only: [:page_not_found, :server_error]

    def page_not_found
         @status = 404
         @layout = "application"
         @template = "not_found_error"
    end

    def server_error
       @status = 500
       @layout = "error"
       @template = "internal_server_error"
    end

    private

    def return_errors
        respond_to do |format|
              format.html { render template: 'errors/' + @template, layout: 'layouts/' + @layout, status: @status }
              format.all  { render nothing: true, status: @status }
        end
    end
end

This gives you the ability to create two views to support the requests:

#app/views/errors/not_found_error.html.erb
Sorry, this page was not found (<%= @status %> Error).

#app/views/errors/internal_server_error.html.erb
Sorry, our server had an error (<%= @status %> Error).

An important note is that you have to include a custom layout for 50x errors:

#views/layouts/error.html.erb
<!DOCTYPE html>
<html>
<head>
  <title><%= action_name.titleize %> :: <%= site_name %></title>
  <%= csrf_meta_tags %>
  <style>
    body {
        background: #fff;
        font-family: Helvetica, Arial, Sans-Serif;
        font-size: 14px;
    }

    .error_container {
        display: block;
        margin: auto;
        margin: 10% auto 0 auto;
        width: 40%;
    }
    .error_container .error {
        display: block;
        text-align: center;
    }
    .error_container .error img {
        display: block;
        margin: 0 auto 25px auto;
    }
    .error_container .message strong {
        font-weight: bold;
        color: #f00;
    }
    .error_container .contact_info {
        display: block;
        text-align: center;
        margin: 25px 0 0 0;
    }
    .error_container .contact_info a {
        display: inline-block;
        margin: 0 5px;
        opacity: 0.4;
        transition: opacity 0.15s ease;
    }
    .error_container .contact_info a:hover {
        opacity: 0.8;
    }

  </style>
</head>
<body>

    <div class="error_container">
        <%= yield %>
    </div>

</body>
</html>

This layout is essential. It has to have inline styles.

You need to make sure your application doesn't invoke any extra dependencies (DB requests / erroneous code), which could prevent the 500 exception view from loading. Keeping a base level layout achieves this.

Using the above code should, at least, give you custom error pages.

--

2. Controller

The second method is more intricate. If you use it properly, it gives you access to the actual exception data.

#config/environments/production.rb
config.exceptions_app = ->(env) { ExceptionsController.action(:show).call(env) }

#app/controllers/exceptions_controller.rb
class ExceptionsController < ActionController::Base

     #Response
     respond_to :html, :xml, :json

     #Details
     before_action :status

     #Layout
     layout :layout

     #######################

     #Show
     def show
           respond_with status: @status
     end

     #######################

    protected

    #Info
    def status
         @exception  = env['action_dispatch.exception']
         @status     = ActionDispatch::ExceptionWrapper.new(env, @exception).status_code
         @response   = ActionDispatch::ExceptionWrapper.rescue_responses[@exception.class.name]
   end

   #Format
  def details
    @details ||= {}.tap do |h|
       I18n.with_options scope: [:exception, :show, @response], exception_name: @exception.class.name, exception_message: @exception.message do |i18n|
               h[:name]    = i18n.t "#{@exception.class.name.underscore}.title", default: i18n.t(:title, default: @exception.class.name)
               h[:message] = i18n.t "#{@exception.class.name.underscore}.description", default: i18n.t(:description, default: @exception.message)
           end
      end
  end
  helper_method :details

     #######################

     private

     #Layout
     def layout_status
          @status.to_s == "404" ? "application" : "error"
     end
end

As you can see, this controller pulls the error details straight from the middleware. This provides maximum flexibility when using deploying the views.

This controller also allows you to run your errors through the show view only, allowing you to limit the number of files you have to manage:

#app/views/exceptions/show.html.erb
<% = details[:name] %>
<%=  details[:message] %>

Of course, you'll also need a custom layout for your 500 errors:

#views/layouts/error.html.erb
<!DOCTYPE html>
<html>
<head>
  <title><%= action_name.titleize %> :: <%= site_name %></title>
  <%= csrf_meta_tags %>
  <style>
    body {
        background: #fff;
        font-family: Helvetica, Arial, Sans-Serif;
        font-size: 14px;
    }

    .error_container {
        display: block;
        margin: auto;
        margin: 10% auto 0 auto;
        width: 40%;
    }
    .error_container .error {
        display: block;
        text-align: center;
    }
    .error_container .error img {
        display: block;
        margin: 0 auto 25px auto;
    }
    .error_container .message strong {
        font-weight: bold;
        color: #f00;
    }
    .error_container .contact_info {
        display: block;
        text-align: center;
        margin: 25px 0 0 0;
    }
    .error_container .contact_info a {
        display: inline-block;
        margin: 0 5px;
        opacity: 0.4;
        transition: opacity 0.15s ease;
    }
    .error_container .contact_info a:hover {
        opacity: 0.8;
    }

  </style>
</head>
<body>

    <div class="error_container">
        <%= yield %>
    </div>

</body>
</html>

Production

This only works in production.

This is good, because you still get to dissect your errors in development, whilst having a branded production error pages.

If you want to have it working in development (so you can tweak etc), you just have to change a single setting in config/environments/development.rb:

#config/environments/development.rb
config.consider_all_requests_local  = false # true - treats all requests as production (for testing)

Gems

There are a number of gems which provide this functionality.

One of the more popular, and most effective, is called exception_handler. This uses the "controller" method above but with several extensions, including a database component (to store the errors) and an email notifier.

Written by a StackOverflow participant, Richard Peck, it allows you to create branded exception pages in your Rails apps without having to create a controller etc:

enter image description here

Installing it is very simple:

#Gemfile
gem "exception_handler", "~0.4"

$ bundle install

It includes its own controller, views and model (which you can all change), and gives you proven extensibility in order to create your own error pages.

If you want to change the configuration options of the gem, you just have to use rails g exception_handler:install. This will create an initializer which will allow you to change its features:

enter image description here

Whilst you can generate the other components of the gem (if you wanted to edit them), you can see how it works from the controller and engine code:

enter image description here

enter image description here

This gem is readily supported and works with Rails 4+.

You can download it from RubyGems or Github.

There's also a great resource for this on StackOverflow.

Comments

Loading comments...

Level Up Your Dev Skills & Income 💰💻

Learn how to sharpen your programming skills, monetize your expertise, and build a future-proof career — through freelancing, SaaS, digital products, or high-paying jobs.

Join 3,000+ developers learning how to earn more, improve their skills, and future-proof their careers.

Custom Error Pages in Rails 4+ | Devmystify