Ruby – Why U No Have Nested Exceptions?

Why U No

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:

begin
  SomeExternalLibrary.do_stuff
rescue => e
  raise
end

This doesn’t really win us anything. Better yet you would raise one of your own custom error types.

begin
  SomeExternalLibrary.do_stuff
rescue => e
  raise MyNamespace::MyError.new
end

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

Egg

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:

require 'nestegg'

class MyError < StandardError
  include Nestegg::NestingException
end

begin
  1/0
rescue => e
  begin
    raise MyError.new("Number errors will be caught", e)
  rescue => e
    begin
      raise MyError.new("Don't need to let MyError bubble up")
    rescue => e
      raise MyError.new("Last one for sure!")
    end
  end
end

It would produce a stack trace like this:

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:

class MyError < StandardError
  include Nesty::NestedError
end

Our stack trace will now be:

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.:

begin
  1/0
rescue => e
  e.message = "foobar"
  e.set_backtrace(['hello', 'world'])
  raise e
end

This produces:

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.

def set_backtrace(backtrace)
  @raw_backtrace = backtrace
  if nested
    backtrace = backtrace - nested_raw_backtrace
    backtrace += ["#{nested.backtrace.first}: #{nested.message}"]
    backtrace += nested.backtrace[1..-1] || []
  end
  super(backtrace)
end

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

  • ryenus

    Oh, if possible please change the url in my previous comment from ‘https’ to ‘http’, didn’t notice that https won’t work. Thank you

    • http://skorks.com/ Alan Skorkin

      Your previous comment accidentally got into the Disqus spam queue. I’ve pulled it out of there, so it should now show up and presumably you should be able to edit it yourself (since I don’t see any way for me to do it).

  • Brian Burns

    Nice. I’ve had some good fun with this myself :)

    https://gist.github.com/burns/4429716

  • http://greyblake.com/ Sergey Potapov

    Looks great! Thanks.

  • http://twitter.com/zekefast Zeke Fast

    Thanks for you work on nesty and it’s extraction.

    One thing that is missed is error class information (compared to nestegg). I suppose it could be useful, especially in cases when error message is empty.

    Is there a way to add class name to error messages? Thank you in advance!

    • http://skorks.com/ Alan Skorkin

      Yeah that’s a very good idea. I’ll add it.

  • http://twitter.com/zekefast Zeke Fast

    Don’t you think to submit you code to foundation like “facets” to make this behavior standardized and prevent future attempts of reimplementing this?

  • http://skorks.com/ Alan Skorkin

    That’s a great idea.

  • Pingback: Reggie's picayune » Intercepting Ruby exceptions

  • http://forumincontri.eu/ ForumIncontri.eu

    thank you!