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)