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 type of exceptions that are thrown in your application. For example, you want to rescue a 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 a 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 given message pattern should be catched. Any other exception should be released (or to use a 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 more recent exception.

Unfortunately, the else statement won't as expected. The exception is correctly rethrown but it isn't catched 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 enter in 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 pass 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 work 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 integrational 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