in which an opportunity for exceptional confusion presents itself

«

»

So I've noticed there seems to be a fair amount of confusion in the Ruby world about exception hierarchies. A number of libraries I use happen to use them in ways that cause problems—usually in ways that surface at the worst possible times near the end of long batch operations. So here's a little primer to refresh your memory and hopefully save some of my sanity if I ever use one of your gems.

Exception is the root class for the whole exception hierarchy. I see a lot of code that subclasses Exception for regular non-fatal exceptions. It's not obvious, but this is really not how it's meant to be used. Imagine you are building a restful interface. The following code will bring down the application:

# This is wrong! Inherit from StandardError.
class GetSomeRest < Exception; end

begin
  if (7 .. 11).include? Time.now.hour
    perform_restful_operation
  else
    raise GetSomeRest
  end
rescue
  puts 'You really should not stay up so late.'
end

This is because rescue will only capture errors that descend from StandardError by default. We should be subclassing StandardError here, as well as for anything else that's non-fatal. Save Exception subclasses for very serious things. Running out of memory should raise an Exception. When a user presses control-c, it raises Interrupt, which is descended directly from Exception rather than StandardError. Sending a Unix kill signal to a process does the same thing. If you rescue Exception or Interrupt, then you have to resort to kill -9 to stop your application externally, leaving it with no chance to clean up after yourself.

Unfortunately, we live in an imperfect world, and we have to deal with libraries that misuse the exception hierarchy. The first thing you should do when you encounter one of these misuses is submit a patch to the offending library. (You could even include a link to this post.) But it's not always possible to get it fixed, so here's the workaround I've been using:

begin
  perform_possibly_problematic_process
rescue Exception => e
  raise e unless e.is_a? StandardError or e.is_a? GetSomeRest
  @log.warn e.message # or whatever
end

If you're not sure of all the problematic Exceptions a piece of code could raise, you could just rescue Exception and re-raise e if it's an Interrupt, but this might swallow some other legitimate serious problems, so it's best to be specific.

Tell your friends to use Ruby's exception hierarchy as it was intended! Update: Don't feel bad if you didn't know this, apparently even the Lucky Stiff himself has fallen into this trap.

« older | 2008-09-23T10:07:01Z | newer »