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 # ...
This works for an exception raised specifically by the body of one controller action, but becomes tedious and repetitive if multiple actions and 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

# ...


def render_404_page
  render # ...
This is a more universal solution to rescuing controller exceptions, regardless of their source. But it still requires some boilerplate to implement the 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 = {
    => :not_found,
    => :unprocessable_entity,
    => :bad_request,
    => :not_found,
  # ...
Action Dispatch’s built-in exception → error page mappings (an abbreviated list).

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
Rails renders the HTTP status code in 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
These headers ensure that errors will be rendered as JSON. The X-Requested-With header prevents the Rails web-console debugger from being triggered when consider_all_requests_local=false. It can be omitted otherwise.


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:

Share this? Copy link

Feedback? Email me!

Hi! 👋 I’m Matt Brictson, a software engineer in San Francisco. This site is my excuse to practice UI design, fuss over CSS, and share my interest in open source. I blog about Rails, design patterns, and other development topics.

Recent articles

View all posts →

Open source projects


App template for Rails 7 projects; best practices for TDD, security, deployment, and developer productivity. Now with optional Vite integration! ⚡️

Updated 1 month ago


A friendly CLI for deploying Rails apps ✨

Updated 20 days ago

More on GitHub →