If you've worked with Ruby's exceptions before, you know you can specify which exceptions get rescued and which are not:
begin
raise ArgumentError
rescue ArgumentError
# Rescues the `ArgumentError`
end
...and you probably know that when you rescue a "parent" you rescue all of its "children" as well.
begin
raise ArgumentError
rescue StandardError
# Rescues `ArgumentError`, because it inherits from
# `StandardError`
end
When I say "parent" and "child" I'm simply referring to class inheritance. Somewhere deep in the Ruby source code there is something equivalent to this:
class ArgumentError < StandardError
...
end
An interesting trick
Here's my question: how does Ruby know if any given exception inherits from the class you specified?
The most obvious approach would be to use the is_a?
or kind_of?
method. We could imagine it looking like this:
if the_exception.is_a?(StandardError)
# do the rescue
end
But that's not what happens. Instead, Ruby uses the more interesting ===
operator.
if StandardError === the_exception
# do the rescue
end
If you've never used a === b
, it usually answers the question "does a inherently belong to the group defined by b"? Here are some examples:
(1..10) === 5 # true
('a'..'f') === "z" # false
String === "hello" # true
String === 1 # false
/[0-9]{3}/ === "hello123" # true
/[0-9]{3}/ === "hello" # false
Because ===
is just an ordinary ruby method like ==
, we can define it ourself:
class RedThings
def self.===(thing)
thing.color == :red
end
end
So, what do we know? We know that rescue
uses ===
to determine which exceptions get rescued. And we know that we can define our own ===
method. That means we can create a class that decides on-the-fly which exceptions are rescued:
class SevereMatcher
def self.===(exception)
exception.message =~ /severe/
end
end
begin
raise RuntimeError, "Something severe happened"
rescue SevereMatcher
# rescues all exceptions with the word "severe" in
# the message, regardless of class.
end
Once you know this trick, the only limit is your imagination.
Conclusion
I'll admit: you may not ever need to create a dynamic exception matcher. But this is a really interesting example of how a seemingly-trivial implementation detail like using ===
instead of kind_of?
makes Ruby much more flexible and interesting.