Rails’ standard error handling is pretty weak (and ugly) out of the box:
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:
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:
- Send the exception to the routes (which then refers onto a specific
controller/action
)- 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:
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:
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:
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.