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!