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:
- A method signature like
def rescue_from(*klasses)
takes a variable number of arguments. Array#extract_options!
is an idiomatic way to get options from an arguments array.- You can determine if a class is a subclass using
klass <= Exception
. rescue nil
will swallow exceptions
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.
More articles in this series
- Reading Rails - HTTP DELETEs With a Link
- Reading Rails - Time Travel
- Reading Rails - TimeWithZone
- Reading Rails - How Does MessageEncryptor Work?
- Reading Rails - How Does MessageVerifier Work?
- Reading Rails - How Do Batched Queries Work?
- Reading Rails - The Adapter Pattern
- Reading Rails - Errors and Validators
- Reading Rails - How Validations Are Configured
- Reading Rails - Concern
- Reading Rails - More Migrations
- Reading Rails - Migrations
- Reading Rails - Attribute Methods
- Reading Rails - Change Tracking
- Reading Rails - Handling Exceptions