Exception Causes in Ruby 2.1

Sometimes when rescuing an exception in Ruby, it’s useful to handle the error scenario by raising another, different exception. As an example, we may want to add domain-specific failure information before passing the error on to client code.

begin
  # ...
rescue KeyError
  raise MyLib::Error, "Bad key: #{key}. Valid keys are: #{valid_keys}"
end

Exceptional Ruby book

The trouble with this technique is that it throws away all the information held by the original exception. This makes debugging harder, as there’s no stack trace to follow back to the root cause of the failure.

In Exceptional Ruby I demonstrated how to write nested exceptions in Ruby. A nested exception carries an optional field pointing back to an “original” exception. Here’s the example from the book:

class MyError < StandardError
  attr_reader :original
  def initialize(msg, original=$!)
    super(msg)
    @original = original;
  end
end

begin
  begin
    raise "Error A"
  rescue => error
    raise MyError, "Error B"
  end
rescue => error
  puts "Current failure: #{error.inspect}"
  puts "Original failure:  #{error.original.inspect}"
end
# >> Current failure: #<MyError: Error B>
# >> Original failure:  #<RuntimeError: Error A>

The implementation of MyError uses a slightly sneaky trick. The initializer uses $! as the default value for original. $!, aka $ERROR_INFO, is a special Ruby variable which always points to the current exception if an exception is presently being raised. If no exception is being raised, it is nil.

def initialize(msg, original=$!)
  super(msg)
  @original = original;
end

This is why, in the example above, we’re able to raise MyError in the usual way, without explicitly initializing it with an original error. By defaulting to $!, the MyError initializer automatically picks up original from the environment.

Until today, a user-defined nested exception class such as this one was the only way to capture and retain information about an original exception that triggered a secondary exception. But today Ruby 2.1 dropped. One of the new features is a new method #cause on the base Exception class.#cause is automatically filled-in by raise (or fail), based on the value of $! just like our MyError implementation.

Let’s try it out:

begin
  begin
    raise "Error A"
  rescue => error
    raise "Error B"
  end
rescue => error
  puts "Current failure: #{error.inspect}"
  puts "Original failure:  #{error.cause.inspect}"
end
# >> Current failure: #<RuntimeError: Error B>
# >> Original failure:  #<RuntimeError: Error A>

This is just like our first example, except there’s no need for a special exception class, and we’ve changed .original to .cause.

Unlike MyError, there is no way to explicitly set Exception#cause. It is always implicitly filled in based on the environment it is raised in.

I’m pretty thrilled that this feature has finally made it into Ruby. Hand-rolled nested exception classes are useful for debugging, but they don’t do us any good when trying to debug errors raised in 3rd-party code that doesn’t use them. With the advent of Exception#cause, debugging Ruby exceptions just got a lot easier.

6 comments

  1. Nice. Hey I really like the look of the ‘Exceptional Ruby’ book. is it still appropriate (or at least mostly) material for ruby 2.1?

Comments are closed.