Exceptional Ruby (published in 2011) was my second Avdi Grimm book in the last couple of months. This book is a great way to dip your toes into the exception handling pond without getting scared away. I’m a sucker for books that start off with a coding metaphor, and Avdi didn’t disappoint. In the intro, he compares exception handling to the toys and clothes a kid stuffs in his closet right before Mom comes to inspect his room. Like a kid cleaning his room, a lot of programmers leave exception handling until last minute, more of an “oh crap!” afterthought than an important well-planned piece of your program.
Failure handling is the underdog that finally gets its time to shine in Exceptional Ruby. Avdi provides example after example of great ways to raise and handle exceptions, many of which I never would’ve thought up on my own out of the blue. If you read this book and don’t feel exceptionally enlightened (pun totally intended), you must be an exceptional genius already. (Is it bad form to use the same pun twice in one sentence?)
A Few Quick Tidbits I Never Want to Forget
I’ll let you in on a secret. I mostly write book reviews for my own personal benefit. Sometimes there’s just not enough space in my head to store the info I want to keep in there. Blog posts are an extension of my brain that I can come back and find again later. In Exceptional Ruby, Avdi touched on a few things I know I’ll want to remember:
- In Ruby,
raise
andfail
are synonyms. Neither is a Ruby keyword. Rather, they’re Kernel methods, so they can be overriden! - The currently active exception is always stored in the global variable
$!
. If there is no active exception,$!
will be nil. -
The
begin
keyword is considered by some to be a code smell in Ruby. Instead of peppering your code with lots ofbegin
,rescue
, andend
blocks, take advantage of Ruby’s implicit begin blocks:def foo # main logic rescue # handle failures here. No explicit begin or end necessary, hooray! end
- A bare
rescue
will catch onlyStandardError
and any derived classes. It will not catch these puppies:NoMemoryError
LoadError
NotImplementedError
SignalException
Interrupt
ScriptError
Exceptional Ideas from Exceptional Ruby
Nested Exceptions
In Ruby, it’s possible to raise a new exception while we’re in the process of handling a previously incurred exception. When this occurs, the original exception is thrown away, completely gone, unless you utilize the idea of Nested Exceptions that Avdi introduces in his book.
Nested Exceptions hold a reference to the original exception so that it isn’t thrown away. Here’s how you’d do it:
1 class MyError < StandardError
2 attr_reader :original
3 def initialize(msg, original=$!)
4 super(msg)
5 @original = original;
6 end
7 end
8
9 begin
10 begin
11 raise "Error A"
12 rescue => error
13 raise MyError, "Error B"
14 end
15 rescue => error
16 puts "Current failure: #{error.inspect}"
17 puts "Original failure: #{error.original.inspect}"
18 end
Running this code produces the output:
Current failure: #<MyError: Error B>
Original failure: #<RuntimeError: Error A>
What’s happening here? We created our own error class called MyError
that stores any currently active exception in original
and sends msg
up to its parent StandardError
object via the call to super
. As we’d expect, our current failure is set to Error B (our most recent failure), which is of type MyError
. The key thing to note in this example is that we still have access to Error A through Error B by calling error.original
. (Note that Error A is a RuntimeError
because this is the default exception type when you use just a bare raise
.)
Code Bulkheads
Nobody wants their code to sink like the Titanic. In ship-speak, bulkheads are placed between ship compartments so that a leak in one compartment will not spread to others. This enables a ship to stay afloat even if one of its compartments is completely flooded. The Titanic had inadequate bulkheads, whith turned out to be a devastating design flaw.
Exceptional Ruby discusses the concept of erecting Bulkheads in your code to stop cascading failures. This isolates parts of your codes from others so that a failure in one area doesn’t cause other parts of the ship to go down.
It’s a good idea to place bulkheads between your app and External Services and External processes. One easy way Exceptional Ruby shows us how to do this is to rescue exceptions and write them to a log instead of bringing down the program:
1 begin
2 # External Web Request
3 response = HTTParty.get("http://www.reddit.com/r/pics")
4 # Everyone loves a good HTTParty
5 rescue Exception => error
6 logger.error "External Web Request encountered an Exception."
7 logger.error error.message
8 logger.error error.backtrace.join("\n")
9 end
Circuit Breakers
Another way to handle failures is using the Circuit Breaker pattern, which Avdi references from Michael Nygard’s book Release It! Circuit breakers are essentially a way of counting failures in particular areas of your App.
When a threshold is met for one component of your program, a “circuit breaker” opens and that component isn’t permitted to operate. After a period of time, the circuit breaker enters a half-open state, where one failure can cause it to open again. Normal operation is the “closed” state. Check out Will Sargent’s Ruby Implementation of this pattern on Github.
Allow for User-injected Failure Policies
I love this exceptional method of handling exceptions. (There’s that pesky pun again…) It’s as simple as this - defer to the method caller!
Avdi refers to this as “caller-supplied fallback strategy”. In my previous book review about Confident Ruby, I raved about using the Hash fetch method to assert the presence of hash keys.
In Exceptional Ruby, I learned that you can pass a block to the fetch method that tells it how to respond to failures! This gives the caller the power to dictate the policy for missing keys, instead of having a policy foisted upon them.
1 h.fetch(:required_key) {
2 raise "ZOMG that required key doesn't exist!"
3 }
You can write your own methods so that you let callers determine what to do if something goes awry. Here’s a super simple example to show you how a method can be structured if you want users to provide their own way of handling unexpected behavior:
1 def render_book_details(book)
2 if book.title && book.author
3 "#{book.title} by #{book.author}"
4 else
5 yield
6 end
7 end
Here, I’ve assumed you have a book object with title and author attributes. If an instance of book doesn’t have either of these attributes, we have a problem. The caller needs to provide a way of handling this problem in the form of a code block. There are a couple ways the caller might handle this.
Perhaps you want the render_book_details
method to return a default string if the book title and author aren’t present:
print_book_details(mybook){ "Book title or author not found." }
You could also raise an exception instead of returning a default string. That’s easy too!
print_book_details(mybook){ raise "Book is missing title or author." }
The user can provide any number of code blocks to handle errors in this situation. What’s truly important is that the power lies with the caller, not with the method author.