Rescue Me
By Phil Matarese on November 12th, 2007
Tagged with: ruby, error handling
I'm about to write a cool new program that leverages the power of somebody else's module. But, I have no idea exactly what kind of errors this module I'm using might generate. I know it connects to web servers around the world looking up important information about dinosaurs and mountain ranges and movie stars and some other things that I need to know about. I know that with all that looking-up, a lot can go wrong, so lemme just catch everything...
1 # My erroneous first draft
2 things_to_look_up.each do |thing|
3 begin
4 thing.look_on_internet!
5 rescue
6 # log it so the sysadmin can check on it tomorrow
7 thing.log $!, $@
8 end
9 end
Looks good. All of the errors will be neatly trapped so the rest of the things can be looked up. If anything goes wrong, the rest of the app will keep running, and I can come back to the error tomorrow. This will make my program nice and robust, and everybody will want to be my friend. Life is good.
...
Uh-oh, what's this?
/1.8/timeout.rb:54:in `rbuf_fill': execution expired (Timeout::Error)
from /1.8/timeout.rb:56:in `timeout'
from /1.8/timeout.rb:76:in `timeout'
from /1.8/net/protocol.rb:132:in `rbuf_fill'
from /1.8/net/protocol.rb:86:in `read'
Something's gone wrong! It wasn't supposed to be like this. I wrote robust code! I caught all the errors! I did the right thing! I made friends. Life was good. Now I feel shame, but I don't know why.
There has to be an explanation for this. Ruby is wonderful. Ruby would never let me down. Digging on the Internet, I see that someone's had a similar problem and they've fixed it by adding code directly to protocol.rb. My stomach hurts. This feels wrong. I need some pepto. I don't feel any better as I read the top of protocol.rb and see:
# WARNING: This file is going to remove.
# Do not rely on the implementation written in this file.
Yikes! As the last shreds of hope are leaving me, I tried fixing protocol.rb as described on the Internet. Low and behold, it works! Strange. The fix was to specify that timeout throw a ProtocolError (which inherits from StandardError) rather than the default Timeout::Error. Why did that make the error trappable? Looking further it seems that Timeout::Error inherits from Interrupt, which is a different kind of exception than StandardError. A key difference has revealed itself, but I'm still confused. Back to the Internet...
Ah-ha! The PickAxe tells all:
How does Ruby decide which rescue clause to execute? It turns out that the processing is pretty similar to that used by thecasestatement. For eachrescueclause in thebeginblock, Ruby compares the raised exception against each of the parameters in turn. If the raised exception matches a parameter, Ruby executes the body of therescueand stops looking. The match is made using$!.kind_of?(parameter), and so will succeed if the parameter has the same class as the exception or is an ancestor of the exception. If you write arescueclause with no parameter list, the parameter defaults toStandardError.
Interrupt (and therefore Timeout::Error) doesn't inherit from StandardError, so it won't be matched by the default rescue. Whew! That wasn't so bad. I can put my Ruby files back to the way they started, and implement a fix in my code.
1 # My beautiful and robust code
2 things_to_look_up.each do |thing|
3 begin
4 thing.look_on_internet!
5 rescue StandardError, Interrupt
6 # log it so the sysadmin can check on it tomorrow
7 thing.log $!, $@
8 end
9 end
Before I add this fix back into my project, I need to add a spec that demonstrates the error. No problem with Rspec, just write a quick stub that raises a Timeout::Error.
1 # My erroneous spec
2 it "should handle errors gracefully" do
3 Thing.stub!(:look_on_internet!).and_raise(Timeout::Error)
4 lambda{ Thing.look_up! }.should_not raise_error
5 end
Bonk! Wrong number of arguments? Grr... Timeout::Error requires that an argument be passed to the constructor, and to do that in rspec I need to pass an instance of the exception class. Ugh... Almost there...
1 # My perfectly correct spec
2 it "should handle errors gracefully" do
3 Thing.stub!(:look_on_internet!).and_raise(Timeout::Error.new(''))
4 lambda{ Thing.look_up! }.should_not raise_error
5 end
Done. Success! All tests pass. Code is robust. Happiness returns. Life is good, again. Time for a celebratory hot chocolate!

Don Parish
Thu January 10, 2008 at 15:38
http://pragprog.com/titles/fr_deploy
Phil Matarese
Fri January 11, 2008 at 09:38