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
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.
Awesome!
fantastic article !
Would there be instances when the #cause in fact isn’t the “cause”?
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?
Thanks for showing me $!. Love your book.
No way to manually set exception cause? Isn’t there some ruby hack possible?