Extending Ruby Exceptions

So far the topics we've covered have been interesting and perhaps obscure. But they haven't been dangerous.

Ruby is a supremely flexible language. It lets you modify the behavior of core systems, and exceptions are no different. We can use techniques like monkey-patching to open up whole new realms of possibility when it comes to exception handling.

Extending Ruby's exception system is an interesting exercise. It can even be useful. But most of the time it's a bad idea. Still, we're all adults here so I'll leave it to you to decide what's best for your particular use-case.

» Retrying Failed Exceptions

We covered the retry keyword in Chapter 2. If you use retry in your rescue block it causes the section of code that was rescued to be run again. Let's look at an example.

begin
  retries ||= 0
  puts "try ##{ retries }"
  raise "the roof"
rescue
  retry if (retries += 1) < 3
end

# ... outputs the following:
# try #0
# try #1
# try #2

» The Problem With retry

While retry is great it does have some limitations. The main one being that the entire begin block is re-run.

For example, imagine that you're using a gem that lets you post status updates to Twitter, Facebook, and lots of other sites by using a single method call. It might look something like this:

SocialMedia.post_to_all("Zomg! I just ate the biggest hamburger")

# ...posts to Twitter API
# ...posts to Facebook API
# ...etc

If one of the APIs fails to respond, the gem raises a SocialMedia::TimeoutError and aborts. If we were to catch this exception and retry, we'd wind up with duplicate posts because the retry would start over from the beginning.

begin
  SocialMedia.post_to_all("Zomg! I just ate the biggest hamburger")
rescue SocialMedia::TimeoutError
  retry
end

# ...posts to Twitter API
# facebook error
# ...posts to Twitter API
# facebook error
# ...posts to Twitter API
# and so on

Wouldn't it be nice if we were able to make the gem only retry the failed requests? Fortunately for us, Ruby allows us to do exactly that.

» Continuations to the Rescue

Continuations tend to scare people. They're not used very frequently and they look a little odd. But once you understand the basics they're really quite simple.

A continuation lets you jump to a location in your code. It's kind of like a goto statement in BASIC.

Let's use continuations to implement a simple loop:

require "continuation"

# Initialize our counter
counter = 0

# Define the location to jump to
continuation = callcc { |c| c } 

# Increment the counter and print it
puts(counter += 1)

# Jump to the location above
continuation.call(continuation) if counter < 3

When you run this, it produces the following output:

1
2
3

You may have noticed a few things:

  • Continuations require a lot of ugly boilerplate.
  • We use the callcc method to create a Continuation object. There's no clean OO syntax for this.
  • The first time the continuation variable is assigned, it is set to the return value of callcc 's block. That's why the block has to be there.
  • Each time we jump back to the saved location, the continuation variable is assigned whatever argument we pass the call method. We don't want it to change, so we do continuation.call(continuation).

» Reimagining retry

We're going to use continuations to add an skip method to to all exceptions. The example below shows how it should work. Whenever I rescue an exception I should be able to call skip, which will cause the code that raised the exception to act like it never happened.

begin
  raise "the roof"
  puts "The exception was ignored"
rescue => e
  e.skip
end

# ...outputs "The exception was ignored"

To do this I'm going to have to commit a few sins. Exception is just a class. That means I can monkey-patch it to add a skip method.

class Exception
  attr_accessor :continuation
  def skip
    continuation.call
  end
end

Now we need to set the continuation attribute for every exception.

The code below is taken almost verbatim from Advi Grimm's excellent slide deck Things You Didn't know about Exceptions. I just couldn't think of a better way to implement it than this:

require 'continuation'
module StoreContinuationOnRaise
  def raise(*args)
    callcc do |continuation|
      begin
        super
      rescue Exception => e
        e.continuation = continuation
        super(e)
      end
    end
  end
end

class Object
  include StoreContinuationOnRaise
end

Now I can call the skip method for any exception and it will be like the exception never happened.

» Logging Local Variables on Raise

If you've ever wished that your exceptions contained a more complete representation of program state, you might be interested in this technique for logging local variables at the time an exception was raised.

This technique is not suitable for production. It imposes a large performance penalty on your entire app, even when no exceptions occur. Moreover, it relies on the binding_of_caller gem which is not actively maintained.

» Introducing binding_of_caller

At any given moment, your program has a certain "stack." This is simply the list of currently "in-progress" methods.

In Ruby, you can examine the current stack via the Kernel#caller method. Here's an example:

def a
  b()
end

def b
  c()
end

def c
  puts caller.inspect 
end

a()

# Outputs:
# ["caller.rb:11:in `b'", "caller.rb:4:in `a'", "caller.rb:20:in `<main>'"]

A binding is a snapshot of the current execution context. In the example below, I capture the binding of a method, then use it to access the method's local variables.

def get_binding
  a = "marco"
  b = "polo"
  return binding
end

my_binding = get_binding

puts my_binding.local_variable_get(:a) # "marco"
puts my_binding.local_variable_get(:b) # "polo"

The binding_of_caller gem combines these two concepts. It gives you access to the binding for any level of the current execution stack. Once you have the binding, it's possible to access local variables, instance variables and more.

In the following example, we use the binding_of_caller gem to access local variables in one method while inside another method.

require "rubygems"
require "binding_of_caller"

def a
  fruit = "orange"
  b()
end

def b
  fruit = "apple"
  c()
end

def c
  fruit = "pear"

  # Get the binding "two levels up" and ask it for its local variable "fruit"
  puts binding.of_caller(2).local_variable_get(:fruit) 
end

a() # prints "orange"

This is really cool. But it's also disturbing. It violates everything we've learned about separation of concerns. It's going to get worse before it gets better. Let's suppress that feeling for a bit and press on.

» Replacing raise

One often-overlooked fact about raise is that it's simply a method. That means that we can replace it.

In the example below we create a new raise method that uses binding_of_caller to print out the local variables of whatever method called raise.

require "rubygems"
require "binding_of_caller"

module LogLocalsOnRaise
  def raise(*args)
    b = binding.of_caller(1)
    b.eval("local_variables").each do |k|
      puts "Local variable #{ k }: #{ b.local_variable_get(k) }"
    end
    super
  end
end

class Object
  include LogLocalsOnRaise
end

def buggy
  s = "hello world"
  raise RuntimeError
end

buggy()

Here's what it looks like in action:

Example exception logs

» Logging All Exceptions With TracePoint

TracePoint is a powerful introspection tool that has been part of Ruby since version 2.0.

It allows you to define callbacks for a wide variety of runtime events. For example, you can be notified whenever a class is defined, whenever a method is called or whenever an exception is raised. Check out the TracePoint documentation for even more events.

Let's start by adding a TracePoint that is called whenever an exception is raised and writes a summary of it to the log.

tracepoint = TracePoint.new(:raise) do |tp|
  # tp.raised_exeption contains the actual exception 
  # object that was raised!
  logger.debug tp.raised_exception.object_id.to_s +
    ": " + tp.raised_exception.class +
    " " + tp.raised_exception.message
end

tracepoint.enable do
  # Your code goes here. 
end

When I run this, every exception that occurs within the enable block is logged. It looks like this:

Logging every use of `raise` in the rendering process