One of the nice things about working with rails is that when something goes wrong in development, you get a really nice error detail page. You get a nice backtrace, with the parts relevant to your app highlighted. You can see the params that got posted, as well as inspect environment and session variables.

Today we're going to take a look at how these fancy error pages work.


Cracking open Actionpack

The file we're going to be mostly concerned with today is actionpack/lib/action_dispatch/middleware/debug_exceptions.rb. It does most of the heavy lifting when it comes to displaying those development mode error pages. If you're curious about where the production mode error screens come from, check out public_exceptions.rb.

Rack Middleware

If you're not familiar with rack middleware, the concept is simple. It allows you to intercept HTTP requests before they get to your app, and to intercept the app's output before it goes back to the user.

Here's a simple middleware that doesn't do anything interesting.

class MyMiddleware
  def initialize(app)
    @app = app

  def call(env)

Rescuing all exceptions via rack middleware

Any exception that occurs in your app occurs as a result of calling So rescuing all exceptions in a rack app is as simple as adding a rescue clause to the middleware.

def call(env)
rescue StandardError => exception
  # this is a method we have to provide to generate the exception page
  render_exception(env, exception)

Anything returned from the call method will be treated as if it were a normal web page. So the content returned by render_exception replaces the original response.

Rendering the exception

I've excerpted the render_exception method from ActionDispatch::DebugExceptions. As you can see, it simply pulls the relevant data from the exception and feeds it into an ERB template.

def render_exception(env, exception)
  wrapper =, exception)
  log_error(env, wrapper)

  if env['action_dispatch.show_detailed_exceptions']
    request =
    template =[RESCUES_TEMPLATE_PATH],
      request: request,
      exception: wrapper.exception,
      application_trace: wrapper.application_trace,
      framework_trace: wrapper.framework_trace,
      full_trace: wrapper.full_trace,
      routes_inspector: routes_inspector(exception),
      source_extract: wrapper.source_extract,
      line_number: wrapper.line_number,
      file: wrapper.file
    file = "rescues/#{wrapper.rescue_template}"

    if request.xhr?
      body = template.render(template: file, layout: false, formats: [:text])
      format = "text/plain"
      body = template.render(template: file, layout: 'rescues/layout')
      format = "text/html"
    render(wrapper.status_code, body, format)
    raise exception

def render(status, body, format)
  [status, {'Content-Type' => "#{format}; charset=#{Response.default_charset}", 'Content-Length' => body.bytesize.to_s}, [body]]

Other uses

You can use this rack middleware trick to do lots of interesting things with exceptions. Here at Honeybadger, we use it to intercept errors and record them to our API.  Here's the code we use to do it:

def call(env)
  config.with_request( do
      env['honeybadger.config'] = config
      response =
    rescue Exception => raised
      env['honeybadger.error_id'] = notify_honeybadger(raised, env)

    framework_exception = framework_exception(env)
    if framework_exception
      env['honeybadger.error_id'] = notify_honeybadger(framework_exception, env)