rails
Don’t reinvent the wheel with Rails exception handling
With the built-in rescue_responses
setting, you can map exceptions to error pages in a simple, declarative way. It automatically renders JSON, too!
Exception handling, the hard way
When dealing with third-party or custom exceptions in a Rails controller, your first instinct might be to implement a vanilla Ruby rescue
.
def show
@invoice = ExternalInvoiceApi.find!(params[:id])
rescue InvoiceNotFound
render # ...
end
before_action
methods need rescuing.
If you are an experienced Rails developer, you might also be familiar with the rescue_from declaration.
rescue_from InvoiceNotFound, with: :render_404_page
# ...
private
def render_404_page
render # ...
end
render
method.
In both cases, rescuing the exception means that you are now responsible for rendering the appropriate error page. And what if your controller responds to both HTML and JSON requests? You’ll need to remember to render an error page tailored for each.
Using a built-in error page with rescue_responses
A much easier solution, especially for a typical “404 not found” scenario, is to leverage the built-in numbered error pages. Rails ships with some prefabricated error pages, which are stored in the public
directory:
public/404.html
“The page you were looking for doesn’t exist (404)”public/422.html
“The change you wanted was rejected (422)”public/500.html
“We’re sorry, but something went wrong (500)”
To use them, add your custom exception to config.action_dispatch.rescue_responses
, like this:
config.action_dispatch.rescue_responses["InvoiceNotFound"] = :not_found
The
rescue_responses
setting maps exception names to HTTP status codes. A symbol like :not_found
can be used, as can the numerical value (404). The full list of possible status symbols is based on Rack source code.
Now, whenever an InvoiceNotFound
exception is raised (and not explicitly rescued), Rails will automatically show the public/404.html
error page.
In fact, this is why built-in exceptions like ActiveRecord::RecordNotFound
automatically render an appropriate error page. As explained in the Rails Guides, several exceptions are mapped to HTTP status codes out of the box:
config.action_dispatch.rescue_responses = {
'AbstractController::ActionNotFound'
=> :not_found,
'ActionController::InvalidAuthenticityToken'
=> :unprocessable_entity,
'ActionController::ParameterMissing'
=> :bad_request,
'ActiveRecord::RecordNotFound'
=> :not_found,
# ...
}
Adding a “forbidden” error page
Most Rails apps will need a “You are not allowed to access this page (403)” error page to handle authorization errors. You can use the rescue_responses
mapping and numbered error pages to accomplish this.
For example, when using the pundit gem, authorization errors take the form of Pundit::NotAuthorizedError
. Adding the mapping is straightforward:
config.action_dispatch.rescue_responses["Pundit::NotAuthorizedError"] = :forbidden
Now create a public/403.html
file and Rails will automatically render it whenever a Pundit::NotAuthorizedError
is encountered. (In practice, I will often just copy and paste an existing error page and tweak the text.)
JSON error responses “just work”
The great thing about rescue_responses
is that Rails knows to render the error as JSON when the client requests it. That way, an API client will get a machine-readable response instead of the contents of public/403.html
, for example.
If the request contains the Accept: application/json
header, then Rails will render the error response like this:
{
"error": "Not Found",
"status": 404
}
status
and the exception message in error
. This all comes out-of-the-box when you use rescue_responses
.
Testing your error pages
Whether you’re configuring error handling using rescue_responses
, or building custom responses using rescue_from
, you’ll need a way to test the results. Here are some tips.
Set consider_all_requests_local = true
(temporarily)
By default, Rails will show you stack traces and other troubleshooting tools in development, but not in production. This applies to rescue_responses
as well, which makes it difficult to judge exactly what end-users will see when an error occurs.
To mimic the production settings and ensure that you are seeing your custom error responses exactly as an end-user would, you can temporarily modify this setting:
config.consider_all_requests_local = false
Setting this to
false
will display errors as they would be shown in production. You’ll need to restart the Rails server after modifying this setting.
Send JSON-appropriate headers
When testing how error responses will be handled for JSON clients, make sure you configure your API client (e.g. Postman) with the following headers:
Accept: application/json
X-Requested-With: XMLHttpRequest
X-Requested-With
header prevents the Rails web-console
debugger from being triggered when consider_all_requests_local=false
. It can be omitted otherwise.
Conclusion
The rescue_responses
mapping is often overlooked. Before writing a custom rescue
or rescue_from
handler in your Rails controller, consider whether the exception could be handled by a generic HTTP status code and error page. If so, a 1-line addition to rescue_responses
in your config/application.rb
might be all you need.
For more background, check out:
- Configuring Rails Applications (Rails Guides)
- Rack HTTP status codes (GitHub)
- Pundit: Rescuing a denied Authorization in Rails (GitHub)