Advanced Rescue & Raise

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")