Back in 2011 I was doing research for the talk that became Exceptional Ruby, and Jim Weirich was nice enough to let me pick his brain on the topic. I was reminded of this email today, and thought I’d share it. There’s a lot of accumulated wisdom about dealing with failure in Ruby (or any language) packed into this short email.
Here’s my basic philosophy (and other random thoughts) on exceptions.
When you call a method, you have certain expectations about what the method will accomplish. Formally, these expectations are called post-conditions. A method should throw an exception whenever it fails to meet its postconditions.
To effectively use this strategy, it implies you must have a small understanding of Design by Contract and the meaning of pre- and post-conditions. I think that’s a good thing to know anyways.
Here’s some concrete examples. The Rails model save method:
model.save! -- post-condition: The model object is saved.
If the model is not saved for some reason, then an exception must be raised because the post-condition is not met.
model.save -- post-condition: (the model is saved && result == true) || (the model is not saved && result == false)
If save doesn’t actually save, then the returned result will be false , but the post-condition is still met, hence no exception.
I find it interesting that the save! method has a vastly simpler post-condition.
On the topic of rescuing exceptions, I think an application should have strategic points where exceptions are rescued. There is little need for rescue/rethrows for the most part. The only time you would want to rescue and rethrow is when you have a job half-way done and you want to undo something so avoid a partially complete state. Your strategic rescue points should be chosen carefully so that the program can continue with other work even if the current operation failed. Transaction processing programs should just move on to the next transaction. A Rails app should recover and be ready to handle the next http request.
Most exception handlers should be generic. Since exceptions indicate a failure of some type, then the handler needs only make a decision on what to do in case of failure. Detailed recovery operations for very specific exceptions are generally discouraged unless the handler is very close (call graph wise) to the point of the exception.
Exceptions should not be used for flow control, use throw/catch for that. This reserves exceptions for true failure conditions.
(An aside, because I use exceptions to indicate failures, I almost always use the fail keyword rather than the raise keyword in Ruby. Fail and raise are synonyms so there is no difference except that fail more clearly communcates that the method has failed. The only time I use raise is when I am catching an exception and re-raising it, because here I’m *not* failing, but explicitly and purposefully raising an exception. This is a stylistic issue I follow, but I doubt many other people do).
There you have it, a rather rambling memory dump on my thoughts on exceptions.
We will all miss Jim for a very long time 🙁
Excellent article. RuntimeError and Fail are lacking in the Ruby code I see. Instead I see Exception and raise….
I don’t always use errors for control flow, but when I do, I catch RuntimeError and I fail branches.
By the way that was a joke. I don’t like to use errors as control flow at all! 😀
Excellent write up as always.
Interesting. What do you think of contracts.ruby? http://egonschiele.github.io/contracts.ruby/ for enforcing these post-conditions?
DbC libraries have existed for Ruby since the very early days, and they never seem to catch on. I haven’t looked at Contracts (yet), but I will now. Thanks!
Thanks for the article Avdi!