Reading Rails - Handling Exceptions

We're going to read some of the Rails source code today. The purpose of this is two fold. We'll learn how exception handling works, and in doing so, expand our general knowledge of ruby.

Rails lets you define handlers on your controllers for common exceptions using rescue_from. For instance you could redirect people to your pricing page whenever they try to access a feature they haven't paid for yet.

class ApplicationController

  # Redirect users if they try to use disabled features.
  rescue_from FeatureDisabledError, InsufficientAccessError do |ex|
    flash[:alert] = "Your account does not support #{ex.feature_name}"
    redirect_to "/pricing"
  end
  #...

We'll explore how Rails defines these handlers, matches them to exceptions, and then rescues failing actions with them.

To follow along, open each library in your editor with qwandry, or just look it up on Github.

Defining Handlers

ActiveSupport contains the Rescuable module which defines how errors are handled. The first method to investigate is rescue_from. This method registers handlers for each type of exception you want to rescue, taking either a method name or a block to call:

def rescue_from(*klasses, &block)
  options = klasses.extract_options!

  unless options.has_key?(:with)
    if block_given?
      options[:with] = block
    else
      #...

Firstly, *klasses accepts a variable number of exception classes, so you could call it with rescue_from(FeatureDisabledError, InsufficientAccessError). Each of these will be stored in an array.

Next, notice the use of extract_options!. This is a common idiom for getting an options hash from an array. If the last element in klasses is a hash, it will be popped off. Now Rails will either use the method designated by the :with option, or use the block passed in. This idiom in Rails creates a flexible interface.

Skimming down the method, we see that those classes are each converted to Strings, we'll see why a little later on.

def rescue_from(*klasses, &block)
  #...
    key = if klass.is_a?(Class) && klass <= Exception
      klass.name
    elsif klass.is_a?(String)
      klass
    else
  #...

What you should note here is how Rails determines if klass inherits from Exception. Normally you might check to see if an object is an instance of a certain type using obj.is_a?(Exception), however klass is not an Exception, it's a Class. So how do we find out which type of class it is? Ruby defines comparison operators like <= on Module. If the object on the left is a subclass of the object on the right, it will return true. For example ActiveRecord::RecordNotFound < Exception is true, while ActiveRecord::RecordNotFound > Exception is false.

At the very end of the method, we see that the String representing the exception class is stored for later as two element array:

def rescue_from(*klasses, &block)
  #...
  self.rescue_handlers += [[key, options[:with]]]
end

Now we know how the handlers are stored, but how does Rails find them when it needs to handle an exception?

Finding Handlers

A quick search for rescue_handlers will tell use that handler_for_rescue uses them. We can see that each possible handler is checked until we find one matching the exception:

def handler_for_rescue(exception)
  # We go from right to left because pairs are pushed onto rescue_handlers
  # as rescue_from declarations are found.
  _, rescuer = self.class.rescue_handlers.reverse.detect do |klass_name, handler|
    #...
    klass = self.class.const_get(klass_name) rescue nil
    klass ||= klass_name.constantize rescue nil
    exception.is_a?(klass) if klass
  end
  #...

As the comment says, rescue_handlers are evaluated in reverse order. If two handlers could handle the same exception, the last one defined will be picked. If you defined a handler for ActiveRecord::NotFoundError followed by Exception, the ActiveRecord::NotFoundError handler would never be called because the Exception handler would always match.

Now, what exactly is taking place in the block?

The String klass_name is converted back to an actual class by first looking for it as a constant in the current class, and then as constant defined anywhere in the application. Each step is rescued with a nil. One reason for doing this is that the handler might be defined for a type of exception that hasn't been loaded. For instance, a plugin may define error handling for ActiveRecord::NotFoundError, but you may not be using ActiveRecord. In that case, even referencing the exception would cause an exception. The rescue nil at the end of each line silently swallows exceptions raised if the class cannot be found.

Finally we check to see if this is an instance of the exception class for this handler. If so, the array [klass_name, handler] will be returned. Hop back up and take a look at the _, rescuer = .... This is an example of ruby's array destructuring. _ is a placeholder since what we really want is the second element, the handler.

Rescuing Exceptions

Now we know how an exception handler is found, but how does this get called? To answer this final question, we can hop back up the file and investigate handler_for_rescue. When passed an exception it will try to deal with it by calling the proper handler.

def rescue_with_handler(exception)
  if handler = handler_for_rescue(exception)
    handler.arity != 0 ? handler.call(exception) : handler.call
  end
end

To find how this actually gets used in your controller though, we need to head over to ActionPack. Rails defines a middleware component called ActionController::Rescue. This mixes in the Rescuable module, and uses it in process_action.

def process_action(*args)
  super
rescue Exception => exception
  rescue_with_handler(exception) || raise(exception)
end

Every request Rails gets calls process_action, and if that request causes an exception to be raised, rescue_with_handler will attempt to handle the error.

Using Rescuable Outside of Rails

Rescuable can be mixed into other code. If you want to centralize your exception handling logic, think about reaching for Rescuable. For instance, perhaps you make lots of calls to remote services and don't want to repeat the exception handling logic on each method:

class RemoteService
  include Rescuable

  rescue_from Net::HTTPNotFound, Net::HTTPNotAcceptable do |ex|
    disable_service!
    log_http_failure(@endpoint, ex)
  end

  rescue_from Net::HTTPNetworkAuthenticationRequired do |ex|
    authorize!
  end

  def get_status
    #...
  rescue Exception => exception
    rescue_with_handler(exception) || raise(exception)
  end

  def update_status
    #...
  rescue Exception => exception
    rescue_with_handler(exception) || raise(exception)
  end

end

With a little meta programming you can even avoid the rescue blocks by wrapping existing methods with this pattern.

Recap

ActiveSupport's Rescuable module lets us define exception handlers. ActionController's Rescue middleware catches exceptions, and then tries to handle them.

We also saw that:

Even a small piece of code contains plenty of useful information. Let me know what you would like to explore next, and we'll see what new things can mine from Rails.

blog comments powered by Disqus
Monkey Small Crow Small