Re-raise a Ruby exception in a Rails rescue_from statement

Let's say you are using rescue_from in your Rails application to rescue some types of exceptions that are thrown in your application. For example, you want to rescue an ActiveRecord::StatementInvalid but not every ActiveRecord::StatementInvalid: just those ActiveRecord::StatementInvalid exceptions where the exception message matches a defined pattern.

In this specific case, the following code won't work.

class ApplicationController < ActionController::Base

  # ...

  rescue_from ActiveRecord::StatementInvalid, :with => :rescue_invalid_encoding

  protected

    def rescue_invalid_encoding
      # ...
    end

end

This is because an ActiveRecord::StatementInvalid is a generic error class and the rescue_from statement will catch any ActiveRecord::StatementInvalid indistinctly. But you don't want this, so you'll decide to go ahead and use the old-fashioned if school to filter the exception message.

Only exceptions matching the given message pattern should be caught. Any other exception should be released (or to use technical jargon, rethrown).

class ApplicationController < ActionController::Base

  # ...

  rescue_from ActiveRecord::StatementInvalid do |exception|
    if exception.message =~ /invalid byte sequence for encoding/
      rescue_invalid_encoding(exception)
    else
      raise
    end
  end

  protected

    def rescue_invalid_encoding(exception)
      head :bad_request
    end

end

The way you rethrow an exception in Ruby is calling raise without passing any exception class or message. Ruby will dutifully re-raise the most recent exception.

Unfortunately, the else statement won't work as expected. The exception is correctly rethrown but it isn't caught by the standard Rails rescue mechanism and the standard exception page is not rendered. Also, the exception is completely invisible to any exception logging platform that relies on rescue_action_in_public such as Hoptoad or Exceptional.

The explanation is simple. To prevent an infinite loop, Rails has a special Failsafe mechanism. When an exception occurs in the exception rescue execution, Rails immediately breaks the execution and enters Failsafe mode.

From the Rails log

Processing ApplicationController#index (for 127.0.0.1 at 2009-11-03 23:30:19) [GET]
  Parameters: {"action"=>"index", "controller"=>"welcome"}
/! FAILSAFE /!  Wed Nov 03 23:30:19 +0100 2009
  Status: 500 Internal Server Error
  ActiveRecord::StatementInvalid

In order to invoke the standard rescue mechanism you need to manually call the rescue_action_without_handler(exception) method.

class ApplicationController < ActionController::Base

  # ...

  rescue_from ActiveRecord::StatementInvalid do |exception|
    if exception.message =~ /invalid encoding/
      rescue_invalid_encoding(exception)
    else
      rescue_action_without_handler(exception)
    end
  end

  protected

  def rescue_invalid_encoding(exception)
    head :bad_request
  end

end

A word of warning: This is a Rails internal API so it can change without additional notice in future versions, so be sure to create a test suite to prevent problems when upgrading your Rails version.

Here's an example of an integration test.

require 'test_helper'

class RescuableTest < ActionController::IntegrationTest

  fixtures :all

  test "rescue from ActiveRecord::StatementInvalid" do
    MainController.any_instance.expects(:set_locale).raises(ActiveRecord::StatementInvalid, 'PGError: ERROR: invalid byte sequence for encoding "UTF8": 0xed706')

    get "/"
    assert_response 400
  end

  test "rescue from ActiveRecord::StatementInvalid with re-raise" do
    MainController.any_instance.expects(:set_locale).raises(ActiveRecord::StatementInvalid, 'Global error')

    get "/"
    assert_response 500
    # perhaps you might want to check here additional instance variables
    # or flash messages
  end

end