Jan 15 2009

Aren’t exceptions supposed to make things safer?

Weren’t things easier in the days of error codes?  Then along came exceptions and took errors to a whole new level.  Unfortunately, they also changed the rules of flow control, and somehow made writing ‘safe’ code more difficult.

Cleaning up after an error, especially a fatal error, is very important.  Exceptions, ironically, can make this an even more difficult task, as this article astutely points out.  For those of you too lazy to make the jump, basically imagine a situation where you call several functions in a row that may throw exceptions, each of which requires a unique clean-up.  To handle such a situation, you would require nested try/catch statements, which is just ugly.

Or, better, we can use call-backs to restore state to pre-exception status.  In the article, this was achieved by using a ScopeGuard object, which held a function pointer that would be called on destruction — but only in the case where the object wasn’t ‘dismissed’ first.  If the object is dismissed, it need not be called.

Being a rubyist, I immediately tried to port this concept over to Ruby.  At face, it doesn’t seem quite as useful at first: the ‘Ruby’ way typically has dangerous objects monitor and clean up after themselves using yield statements.  For example, when we open a file, we typically pass a block to be executed with the open file — but only in the case that the file is successfully opened.

On the other hand, imagine the case where we are trying to synchronize writes between two resources.  Perhaps we are using memory as a cache, but also storing to disk.  Let us also assume that failure to write to either resource ends up in an exception, forcing the other resource to roll-back to its previous state.  Handling the nesting of exceptions could get ugly.  Enter scope_guard.

Instead of creating a class like the above article does, I made my scope guard a function that takes an object, function name, and parameters.  If a block is given, the function yields, wrapping the statement in a begin/rescue block.  In the case of an exception, our ‘undo’ function is performed on the object.  Ruby makes it all simple and easy to do.

Imagine usage:

def write(cache, disk, data)
   cache.write(data)
   scope_guard(cache, :roll_back) {
      disk.write(data)
   }
end

Now you can see that if the disk fails to write the data, the scope-guard ensures that the cache is rolled-back.  In the other case, where the cache fails to write, the exception raises before the data is written to disk, so state is maintained.

The system isn’t without flaws, however.  It falls short in one of the simplest of cases: an integer counter.  If I am trying to maintain a count of objects I write to disk, I have to use a wrapper around the basic integer object, because operations on Fixnum return a new object instead of altering the object in question.  Not the end of the world, but not very clean either.  I am currently trying to wrap my head around possible elegant solutions for this.

  • Share/Bookmark