Blog

Exceptional Error Handling in Ruby

Placeholder Avatar
Yuji Yokoo
December 18, 2015

I recently come across some surprising code involving exceptions which prompted me to look a bit deeper into exceptions in Ruby. In this post, I would like to share some of what I found.

Exception vs StandardError

This is where it all started. I had a script that did something like the following (it was not quite this simple, but it shows the important parts):

ruby class MyLib class MyLibBaseError < Exception ; end class MyIOError < MyLibBaseError ; end def do_something raise MyIOError rescue puts "logging error: #{$!.message}" # $! is the last exception raised end end

Calling do_something results in this:

ruby > MyLib.new.do_something MyLib::MyIOError: MyLib::MyIOError from (irb):70:in `do_something' from (irb):76 from /Users/yuji/.rvm/rubies/ruby-2.1.7/bin/irb:11:in `<main>'

This was surprising to me: MyIOError is clearly raised, but puts is not called. This is because the blank rescue only catches StandardError and it’s subclasses, hence, any exception that is not a subclass of StandardError will not be caught by it.

The built-in exceptions are listed in the Ruby documentation for the Exception class.

Possible options for making this catch MyIOError include: * Making MyLibBaseError inherit from StandardError. * Modifying the rescue statement to be more specific about which exception classes to rescue from.

I did both of these, since I wanted MyIOError to be caught in any blank rescue statements around the app. And I also feel that using a blank rescue is not good practice, since I do not want to accidentally catch unintended exceptions.

Begin-rescue-end and def-rescue-end

Typically, rescue is used in the form of begin-rescue-end:

ruby begin do_something rescue => e do_something_with_error(e) end

In addition to this, Ruby also has def-rescue-end in method definitions, for when you want to rescue from the entirety of a method:

ruby def do_rescue do_something rescue => e do_something_with_error(e) end

This is a nicer equivalent to:

ruby def do_rescue begin do_something rescue => e do_something_with_error(e) end end

Default exception and optional arguments

By default, raise raises RuntimeError. If a string is passed to raise, it will raise a RuntimeError with the given string as its message.

ruby > raise "message!" RuntimeError: message!

In addition, optional arguments can be passed to raise: * With 2 arguments, they will be the exception class to raise and the message. * When 3 arguments are given, the first argument is the exception class, the second is the message, and the last one is an array of callback information. ruby > raise ZeroDivisionError, 'cannot divide by zero!', caller ZeroDivisionError: cannot divide by zero! from /Users/yuji/.rvm/rubies/ruby-2.1.7/lib/ruby/2.1.0/irb/workspace.rb:86:in `eval' from /Users/yuji/.rvm/rubies/ruby-2.1.7/lib/ruby/2.1.0/irb/workspace.rb:86:in `evaluate' ...

Multiple rescues

Multiple rescue parts can be used to rescue different exceptions:

ruby def multi_rescue 1 / 0 rescue NameError puts "NameError raised." rescue ZeroDivisionError puts "ZeroDivisionError raised." end

If they are handled exactly the same, they can be rescued together:

ruby def multi_rescue 1 / 0 rescue NameError, ZeroDivisionError => e puts "#{e.class.name} raised. " end

ruby > multi_rescue ZeroDivisionError raised. => nil

Ensure

Regardless of whether an exception is rescued, the code in the ensure section will be executed. This is useful, for example, to close a file that has been opened.

ruby def read_file f = File.open('input_file.txt', 'r') # ... do something rescue # error processing puts 'error handled' ensure f.close puts 'closed file' end

When running this with an error: ruby > read_file error handled closed file => nil And without an error:

ruby > read_file closed file => nil

Else

This is not very common, but else can be used to execute code when nothing is raised. This is a little like else in Ruby’s case-when-else.

ruby def use_else puts "doing something" rescue NameError puts "NameError raised." rescue ZeroDivisionError puts "ZeroDivisionError raised." else puts "nothing raised" ensure puts "ensuring." end

ruby > use_else doing something nothing raised ensuring. => nil ## Raise and fail In Ruby, raise and fail do the same. You can raise an exception or fail with an exception. raise seems to be more commonly used, but it is really up to you to decide which one expresses your intentions better.

ruby > raise 'raise!' RuntimeError: raise! from (irb):19 from /Users/yuji/.rvm/rubies/ruby-2.1.7/bin/irb:11:in `<main>' > fail 'fail!' RuntimeError: fail! from (irb):20 from /Users/yuji/.rvm/rubies/ruby-2.1.7/bin/irb:11:in `<main>'

One-liner

Rescue can be a one-liner:

ruby > puts 1/0 rescue nil # ZeroDivisionError is suppressed here. => nil

This makes it really easy to ignore errors, though it is not possible to specify which exceptions to handle here. So it will catch StandardError and all its subclasses. I would not particularly recommend this, since I feel it is too easy to make mistakes by overusing this.

Ignoring exceptions

Rescuing and not doing anything with the caught exception is a common anti-pattern:

ruby def ignore_error do_something rescue end Things like this often cause problems in tracking down issues later on. I advise against this, and would suggest at least logging the error and re-raising the same exception, or, if you have to, adding a comment to the code so your colleagues know that the exception has intentionally been ignored.

Catch and throw

Although this is not a use of Exception, Ruby also has catch and throw constructs. This may be useful for people who are used to try-catch in other languages. It can also be used to jump out of loops.

ruby def catch_foo catch :foo do puts "start" i = 0 while i < 10 j = 0 while j < 10 puts "i: #{i}, j: #{j}" throw :foo j += 1 end i += 1 end puts "end" end puts "all done!" end

ruby > catch_foo start i: 0, j: 0 all done! => nil

An uncaught throw will result in an ArgumentError:

ruby > throw :foo ArgumentError: uncaught throw :foo

Final Thoughts

Even though there are many different ways to write exception handling code, I would generally stick to basic, simple rules: * Inherit from StandardError when defining your own exception classes. * Specify which exceptions types to rescue. * Use ensure to execute code regardless of whether or not there was an exception. * Use many rescue sections for different errors, and else for when nothing is raised. * Do not use a one-liner rescue. * Do not simply ignore or suppress exceptions.

However, I myself may not always follow all of these rules; exception handling, by definition, is dealing with exceptional situations, and there may be situations where it makes sense not to follow the rules!