Much of the hidden power of Ruby's exception system is contained in the humble rescue
and raise
syntax. In this chapter I'm going to show you how to harness that power. If the beginning of the chapter is old-news to you, stick with it! By the end, we'll be covering new ground.
» Full Rescue Syntax
Simple begin-rescue-end clauses work well for most cases. But occasionally you have a situation that requires a more sophisticated approach. Below is an example of Ruby's full rescue syntax.
begin
...
rescue TooHotError => too_hot
# This code is run if a TooHotError occurs
rescue TooColdError => too_cold
# This code is run if a TooColdError occurs
else
# This code is run if no exception occurred at all
ensure
# This code is always run, regardless of whether an exception occurred
end
As you can see we've introduced a few new keywords:
else
- is executed when no exceptions occur at all.ensure
- is always executed, even if exceptions did occur. This is really useful for actions like closing files when something goes wrong.rescue
- you know what rescue, is, but I just wanted to point out that we're using multiple rescues to provide different behavior for different exceptions.
I find that the full rescue syntax is most useful when working with disk or network IO. For example, at Honeybadger we do uptime monitoring — requesting your web page every few minutes and letting you know when it's down. Our logic looks something like this:
begin
request_web_page
rescue TimeoutError
retry_request
rescue
send_alert
else
record_success
ensure
close_network_connection
else
Here, we respond to timeout exceptions by retrying. All other exceptions trigger alerts. If no error occurred, we save the success in the database. Finally, regardless of what else occurred, we always close the network connection.
» Method Rescue Syntax
If you find yourself wrapping an entire method in a rescue clause, Ruby provides an alternate syntax that is a bit prettier:
def my_method
...
rescue
...
else
...
ensure
...
end
This is particularly useful if you want to return a "fallback" value when an exception occurs. Here's an example:
def data
JSON.parse(@input)
rescue JSON::JSONError
{}
end
» The retry
Keyword
The retry
keyword allows you to re-run everything between begin
and rescue
. This can be useful when you're working with things like flaky external APIs. The following example will try an api request three times before giving up.
counter = 0
begin
counter += 1
make_api_request
rescue
retry if counter <= 3
end
Ruby doesn't have a built-in mechanism to limit the number of retries. If you don't implement one yourself — like the counter
variable in the above example — the result will be an infinite loop.
» Reraising Exceptions
If you call raise
with no arguments, while inside of a rescue block, Ruby will re-raise the original rescued exception.
begin
...
rescue => e
raise if e.message == "Fubar"
end
To the outside world a re-raised exception is indistinguishable from the original.
» Changing Exceptions
It's common to rescue one exception and raise another. For example, ActionView
rescues all exceptions that occur in your ERB templates and re-raises them as ActionView::TemplateError
exceptions.
begin
render_template
rescue
raise ActionView::TemplateError
end
In older versions of Ruby (pre 2.1) the original exception was discarded when using this technique. Newer versions of Ruby make the original available to you via a nested exception system which we will cover in detail in Chapter 4.
» Exception Matchers
In this book, you've seen several examples where we rescue a specific type of exception. Here's another one:
begin
...
rescue StandardError
end
This is good enough 99.99% of the time. But every so often you may find yourself needing to rescue exceptions based on something other than "type." Exception matchers give you this flexibility. When you define an exception matcher class, you can decide at runtime which exceptions should be rescued.
The following example shows you how to rescue all exceptions where the message begins with the string "FOOBAR".
class FoobarMatcher
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 FoobarMatcher
puts "rescued!"
end
» The Mechanism
To understand how exception matcher classes work, you first need to understand how rescue
decides which exceptions to rescue.
In the code below we've told Ruby to rescue a RuntimeError
. But how does ruby know that a given exception is a RuntimeError
?
begin
...
rescue RuntimeError
end
You might think that Ruby simply checks the exception's class via is_a?
:
exception.is_a?(RuntimeError)
But the reality is a little more interesting. Ruby uses the ===
operator. Ruby's triple-equals operator doesn't have anything to do with testing equality. Instead, a === b
answers the question "is b inherently part of a?".
(1..100) === 3 # True
String === "hi" # True
/abcd/ === "abcdefg" # True
All classes come with a ===
method, which returns true if an object is an instance of said class.
def self.===(o)
self.is_a?(o)
end
When we rescue RuntimeError
ruby tests the exception object like so:
RuntimeError === exception
Because ===
is a normal method, we can implement our own version of it. This means we can easily create custom "matchers" for our rescue clauses.
Let's write a matcher that matches every exception:
class AnythingMatcher
def self.===(exception)
true
end
end
begin
...
rescue AnythingMatcher
end
By using ===
the Ruby core team has made it easy and safe to override the default behavior. If they had used is_a?
, creating exception matchers would be much more difficult and dangerous.
» Syntactic Sugar
This being Ruby, it's possible to create a much prettier way to dynamically catch exceptions. Instead of manually creating matcher classes, we can write a method that does it for us:
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
» Advanced Raise
In Chapter 1 we covered a few of the ways you can call raise
. For example each of the following lines will raise a RuntimeError
.
raise
raise "hello"
raise RuntimeError, "hello"
raise RuntimeError.new("hello")
If you look at the Ruby documentation for the raise
method, you'll see something weird. The final variant — my personal favorite — isn't explicitly mentioned.
raise RuntimeError.new("hello")
To understand why, let's look at a few sentences of the documentation:
With a single String argument, raises a RuntimeError with the string as a message. Otherwise, the first parameter should be the name of an Exception class (or an object that returns an Exception object when sent an exception message).
The important bit is at the very end: "or an object that returns an Exception object when sent an exception message."
This means that if raise
doesn't know what to do with the argument you give it, it'll try to call the exception
method of that object. If it returns an exception object, then that's what will be raised.
And all exception objects have a method exception
which by default returns self
.
e = Exception.new
e.eql? e.exception # True
» Raising Non-Exceptions
If we provide an exception
method, any object can be raised as an exception.
Imagine you have a class called HTMLSafeString
which contains a string of text. You might want to make it possible to pass one of these "safe" strings to raise
just like a normal string. To do this we simply add an exception
method that creates a new RuntimeError
.
class HTMLSafeString
def initialize(value)
@value = value
end
def exception
RuntimeError.new(@value)
end
end
raise HTMLSafeString.new("helloworld")