One of the things we almost always do these days when we write our libraries and apps, is use other libraries. Inevitably something will go wrong with those libraries and exceptions will be produced. Sometimes these are expected (e.g. an HTTP client that produces an exception when you encounter a 500 response or a connection timeout), sometimes they are unexpected. Either way you don’t want to allow the exceptions from these external libraries to bubble up through your code and potentially crash your application or cause other weirdness. Especially considering that many of these exceptions will be custom types from the libraries you’re using. No-one wants strange exceptions percolating through their code.
What you want to do, is ensure that all interactions with these external libraries are wrapped in a begin..rescue..end
. You catch all external errors and can now decide how to handle them. You can throw your hands up in the air and just re-raise the same error:
|
|
This doesn’t really win us anything. Better yet you would raise one of your own custom error types.
|
|
This way you know that once you’re past your interfaces with the external libraries you can only encounter exception types that you know about.
The Need For Nested Exceptions
The problem is that by raising a custom error, we lose all the information that was contained in the original error that we rescued. This information would have potentially been of great value to help us diagnose/debug the problem (that caused the error in the first place), but it is lost with no way to get it back. In this regard it would have been better to re-raise the original error. What we want is to have the best of both worlds, raise a custom exception type, but retain the information from the original exception.
When writing escort one of the things I wanted was informative errors and stack traces. I wanted to raise errors and add information (by rescuing and re-raising) as they percolated through the code, to be handled in one place. What I needed was the ability to nest exceptions within other exceptions.
Ruby doesn’t allow us to nest exceptions. However, I remembered Avdi Grimm mentioning the nestegg gem in his excellent Exceptional Ruby book, so I decided to give it a try.
The Problems With Nestegg
Unfortunately nestegg
is a bit old and a little buggy:
- It would sometimes lose the error messages
- Nesting more than one level deep would cause repetition in the stacktrace
I also didn’t like how it made the stack trace look non-standard when including the information from the nested errors. If we take some code similar to the following:
|
|
It would produce a stack trace like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
examples/test1.rb:26:in `rescue in rescue in rescue in <main>': MyError (MyError) from examples/test1.rb:23:in `rescue in rescue in <main>' from examples/test1.rb:20:in `rescue in <main>' from examples/test1.rb:17:in `<main>' from cause: MyError: MyError from examples/test1.rb:24:in `rescue in rescue in <main>' from examples/test1.rb:20:in `rescue in <main>' from examples/test1.rb:17:in `<main>' from cause: MyError: MyError from examples/test1.rb:21:in `rescue in <main>' from examples/test1.rb:17:in `<main>' from cause: ZeroDivisionError: divided by 0 from examples/test1.rb:18:in `/' from examples/test1.rb:18:in `<main>' |
After looking around I found loganb-nestegg. This fixed some of the bugs, but still had the non-standard stack trace and the repetition issue.
When you’re forced to look for the 3rd library to solve a problem, it’s time to write your own.
This is exactly what I did for escort. This functionality eventually got extracted into a gem which is how we got nesty. Its stack traces look a lot like regular ones, it doesn’t lose messages and you can nest exceptions as deep as you like without ugly repetition in the stack trace. If we take the same code as above, but redefine the error to use nesty
:
|
|
Our stack trace will now be:
1 2 3 4 5 6 7 8 |
examples/complex.rb:20:in `rescue in rescue in rescue in <main>': Last one for sure! (MyError) from examples/complex.rb:17:in `rescue in rescue in <main>' from examples/complex.rb:18:in `rescue in rescue in <main>': Don't need to let MyError bubble up from examples/complex.rb:14:in `rescue in <main>' from examples/complex.rb:15:in `rescue in <main>': Number errors will be caught from examples/complex.rb:11:in `<main>' from examples/complex.rb:12:in `/': divided by 0 from examples/complex.rb:12:in `<main>' |
Definitely nicer. We simply add the messages for every nested error to the stack trace in the appropriate place (rather than giving them their own line).
How Nested Exceptions Work
The code for nesty
is tiny, but there are a couple of interesting bits in it worth looking at.
One of the special variables in Ruby is $!
which always contains the last exception that was raised. This way when we raise a nesty
error type, we don’t have to supply the nested error as a parameter, it will just be looked up in $!
.
Ruby always allows you to set a custom backtrace on any error. So, if you rescue an error you can always replace its stack trace with whatever you want e.g.:
|
|
This produces:
1 2 |
hello: divided by 0 (ZeroDivisionError) from world |
We take advantage of this and override the set_backtrace
method to take into account the stack trace of the nested error.
|
|
To produce the augmented stack trace we note that the stack trace of the nested error should always be mostly a subset of the enclosing error. So, we whittle down the enclosing stack trace by taking the difference between it and the nested stack trace (I think set operations are really undervalued in Ruby, maybe a good subject for a future post). We then augment the nested stack trace with the error message and concatenate it with what was left over from the enclosing stack trace.
Anyway, if you don’t want exceptions from other libraries invading your app, but still want the ability to diagnose the cause of the exceptions easily – nested exceptions might be the way to go. And if you do decide that nested exceptions are a good fit, nesty is there for you.
Image by Samuel M. Livingston