Ruby and Nested Exceptions
/Often, one exception causes another.
A library tries to read a configuration file with File.read, which raises an exception of type Errno::ENOENT with the message "No such file or directory @ rb_sysopen". That library then raises another exception to let you know: it couldn't find its configuration, possibly after looking in several different places.
Older versions of Ruby used to throw away this inner exception. The library rescued the "no such file" exception, swallowed it, and raised an entirely new one. Indeed, some libraries still do. Folks like Avdi Grimm and Charles Nutter were in favor of the inner exception sticking around. Ruby isn't the only language to do this. It's common practice in other languages like Java and .NET. You'll even see recommendations for wrapping all exceptions in your library's version, even in Ruby.
And so in recent Ruby, if you raise an exception from the "rescue" block of another, it saves the inner exception. If you rescue the new exception, you can call "cause" on it to find the inner one! (You can also do it differently, but that's documented poorly - I'll show you the secret way to do it if you read all the way to the bottom of this post.)
2.3.1 :003 > begin
2.3.1 :004 > begin
2.3.1 :005 > raise "Inner message"
2.3.1 :006?> rescue
2.3.1 :007?> raise "Outer message"
2.3.1 :008?> end
2.3.1 :009?> rescue
2.3.1 :010?> nest_e1 = $!
2.3.1 :011?> end
=> #<RuntimeError: Outer message>
2.3.1 :012 > nest_e1
=> #<RuntimeError: Outer message>
2.3.1 :014 > nest_e1.cause
=> #<RuntimeError: Inner message>
This means that sometimes you can find really interesting information if you look a bit. If the library handles its "no such file or directory" with a rescue and a raise, the error underneath is captured right in the new exception!
Of course, you have to look for it. You don't see the nested exception unless you call "cause" on an exception:
2.3.1 :013 > raise nest_e1
RuntimeError: Outer message
from (irb):7:in `rescue in irb_binding'
from (irb):4
from /Users/noah.gibbs/.rvm/rubies/ruby-2.3.1/bin/irb:11:in `<main>'
2.3.1 :014 > nest_e1.cause
=> #<RuntimeError: Inner message>
But if you can catch the exception and have a look, you can print it out. That's not terrible, but maybe we can do better.
Customizing with Minitest
I use Minitest, and when I get an exception I often want to see what's gone wrong. Even if Ruby's not showing us the problem, maybe we can hook into our test framework?
As it happens, we definitely can!
# test_helper.rb
class Minitest::UnexpectedError
def message
# Build a chain of exception causes
exc = self.exception
cause_chain = []
loop do
cause_chain.push(exc)
exc = exc.cause
break unless exc
end
bt_lines = cause_chain.map { |c|
[c.message] + Minitest.filter_backtrace(c.backtrace)
}.inject() { |acc, bt| acc + ["... Caused By ..."] + bt }
bt_out = bt_lines.join "\n "
return "#{self.exception.class}: #{self.exception.message}\n #{bt_out}"
end
end
Note that this technique isn't limited to nested exceptions and causes. An exception object can have anything you want, and you can hook into minitest and print out the extra information. Just generate a string of your choice. You're basically writing a Minitest mini-plugin into your test helper, which is a pretty common thing to do...
For nested exceptions, I've already opened a pull request for Minitest - we'll see if it makes it in!
It looks like the Ruby folks also think we should print the causes for exceptions, but just haven't gotten around to it yet...
Secrets
So if you can set the cause by raising your error from the "rescue" clause, that's okay. But what if you want to do it from somewhere else?
Can you pass the cause to the constructor for your new Exception?
Hm... Not so much, it turns out. There was some debate about it in the bug report, but no.
Instead, there's a secret keyword to "raise" that will let you set a cause if $! isn't set, or override it if it is:
raise MyOuterException.new("oh no!"), cause: MyInnerException.new("ducks!")
Shh... Don't tell anybody. It's a secret. I had to get it out of the Ruby source code and tests, so I assume nobody wants you to know...
Why Do I Care?
Now you know about Ruby's nested exceptions. You care if an exception might have extra information you need for debugging - now you know to catch it and print out the exception's cause... And maybe the cause's cause, and so on.
You care if your test library or REPL is catching and printing an exception but doesn't let you see the cause, like Minitest above. But this same problem applies to RSpec, Test::Unit and even irb or pry - if they're printing the exception but not the cause, you don't get to see it.
And you care if you're writing a gem - be sure to raise your exception from the 'rescue' clause so that folks can see what exception caused the exception! See the Secrets section above, in case your gem's structure is a bit more complicated.