When you use a rescue clause in Ruby, you can specify what kinds of exceptions you want to rescue. All you need to do is provide a list of exception classes like so:
begin
raise RuntimeError
rescue RuntimeError, NoMethodError
puts "rescued!"
end
But what if you don't know what the exception class will be at the time that you write the code? The most obvious answer is to rescue all exceptions, perform some kind of test and then re-raise the exceptions that don't pass. Something like this:
begin
raise "FUBAR! The ship's going down!"
rescue => e
raise unless e.message =~ /^FUBAR/
... do something ...
end
But that's so boring! Plus, it's not a very DRY approach. It would be a lot more interesting if we could somehow tell the rescue clause to only rescue exceptions matching our conditions. And since this is Ruby, we can do it!
How rescue matches exceptions
When an exception happens inside of a rescue block, the ruby interpreter checks the exception's class against the list of exception classes you provided. If there's a match, the exception gets rescued.
The matching looks something like this:
exception_classes_to_rescue.any? do |c|
c === raised_exception.class
end
Just like every other operator in Ruby, ===
is simply a method. In this case it's a method of c
. So what could we do if we defined our own ===
method?
In the example below I'm creating a class named Anything
where Anything === x
returns true for any value of x. If I give this class as an argument to rescue, it causes all exceptions to be rescued.
class Anything
def self.===(exception)
true
end
end
begin
raise EOFError
rescue Anything
puts "It rescues ANYTHING!"
end
While there are much better ways to rescue all exceptions, this code is interesting because it shows us two things:
You can give the rescue clause classes that don't inherit from
Exception
, as long as they implement===
If you control
===
, you can control which exceptions are rescued.
Rescuing exceptions based on message
Knowing what we know now, it's simple to write code that only rescues exceptions if the exception's message matches a pattern.
class AllFoobarErrors
def self.===(exception)
# rescue all exceptions with messages starting with FOOBAR
exception.message =~ /^FOOBAR/
end
end
begin
raise EOFError, "FOOBAR: there was an eof!"
rescue AllFoobarErrors
puts "rescued!"
end
Rescuing exceptions based on custom attributes
Since you have access to the exception object, your matcher can use any data contained inside that object.
Imagine for a moment that you have an exception that has a custom attribute called "severity." You'd like to swallow all "low severity" occurrences of the exception, but let pass any "high severity" occurrences. You might implement that like so:
class Infraction < StandardError
attr_reader :severity
def initialize(severity)
@severity = severity
end
end
class LowSeverityInfractions
def self.===(exception)
exception.is_a?(Infraction) && exception.severity == :low
end
end
begin
raise Infraction.new(:low)
rescue LowSeverityInfractions
puts "rescued!"
end
Making it dynamic
All of this is pretty cool, but it does involve a lot of boilerplate code. It seems excessive to have to manually define separate classes for each matcher. Fortunately we can DRY this up quite a bit by using a little metaprogramming.
In the example below, we're defining a method that generates matcher classes for us. You provide the matching logic via a block, and the matching generator creates a new class that uses the block inside of its ===
method.
def exceptions_matching(&block)
Class.new do
def self.===(other)
@block.call(other)
end
end.tap do |c|
c.instance_variable_set(:@block, block)
end
end
begin
raise "FOOBAR: We're all doomed!"
rescue exceptions_matching { |e| e.message =~ /^FOOBAR/ }
puts "rescued!"
end
A grain of salt
Like many cool tricks in Ruby, I can't quite decide if all of this is insanity or a great idea. Maybe it's a little of both. While I definitely wouldn't suggest that you reach for this technique as a first choice, I can see how it would be useful in situations like the one above where you want to rescue exceptions based on severity. In any case, it's another tool in your toolbelt!