Sunday, 4 January 2009

Exception Handling

You can define your own exceptions in Ruby very easily, just extend a suitable class (that is, StandardError or any of its sub-classes):
class MyError < StandardError


To see how exceptions are handled, let us look at this example:
try_counter = 0
try_counter += 1
puts 'Here 1'
raise "Text" unless try_counter > 5
puts 'Here 2'
rescue MyError
puts 'Here 3 - MyError encountered'
rescue StandardError
puts "Here 4 - Other error encountered (#{$!.inspect})" + caller.inspect
puts 'Here 5 - No errors'
puts 'Here 6 - Always done'

Exceptions are handled by blocks. The risky code goes into the first chunk of the block. If an exception is encountered, the program will jump to the end of that chunk and look for an appropriate action. In the above example, it will first look at rescue MyError. If the exception is of the MyError class - or a sub-class - then the program will not run the code folling the rescue. Note that you can list multiple exception classes, separated by commas.

Otherwise, it will look further. The next rescue is for StandardError, so this will catch all errors of the StandardError type or its sub-classes - except for MyClass, as that would have been caught earlier. All rescuable exceptions must inherit from StandardError (but see later), so this is set up to catch all exceptions, but that need not be the case; uncaught exceptions will be passed on up the stack to whatever else might handle them.

The else chunk will get performed after the initial chunk has completed successfully (i.e., without raising an exception). Finally, the ensure chunk gets performed whatever the outcome.

An exception is raised using the raise keyword. Exceptions are just objects, so are instantiated just like any other object. They typically take a string parameter, which you can use to give a descriptive message. Alternatively, you use this form:
raise "Test"
raise MyError, "Text", caller
raise MyError, "Text"
raise "Text" # For the default RuntimeError

You can put a retry in a rescue chunk, as in the example above. The program will jump back to the start of the block, and start again. In the second rescue chunk, there is a raise on its own. This will pass the exception ouside of the block, to be handled by some higher up error handling system.

Also in that chunk, note that the $! code is the exception. If you prefer, you can set your own name for this.
rescue StandardError => err
puts "Here 4 - Other error encountered (#{err.inspect})" + caller.inspect
Here is the output from the first example:
Here 1
Here 3 - MyError encountered
Here 1
Here 3 - MyError encountered
Here 1
Here 3 - MyError encountered
Here 1
Here 3 - MyError encountered
Here 1
Here 3 - MyError encountered
Here 1
Here 2
Here 5 - No errors
Here 6 - Always done

Stack Trace
To see the stack trace, use the backtrace method. This returns an array of strings. To just see the top dozen entries, you could use this:
$!.backtrace[0..12] * "\n"

Exceptions that do not inherit from StandardError
The superclass for all exceptions is Exception. The theory is that all exceptions that a program should be expected to recover from are either StandardError or a subclass of it, which is why I described them as "rescuable exceptions " earlier. If no exception is specified, rescue will default to StandardError.

However, there may be times you want to recover from other exceptions. A particular example I came across is a SyntaxError thrown by ERB. The offending syntax was in a data file, not my application; my application should have caught the error, and reported it back to the user. This is entirely different to a syntax error in my code, and in my opinion ERB should throw its own exceptions that inherit from StandardError. Time-out errors are another example, as discussed here.

To catch all exceptions use:
rescue Exception

The rescue statement modifier
Rescue can be used in a way analogous to if and unless. For example:
test rescue do_stuff

This statement will run the method test, and if it encounters an error (specifically StandardError or subclasses) it will invoke do_stuff. You can use this to assign a default value if a method fails, like this:
s = test rescue "Default value"

The rescue stament returns the value "Default Value". You could write it with brackets, which might help make sense of it:
s = (test rescue "Default value")

You can concatenate statement with semi-colons, but cannot use blocks.
s = test rescue do_stuff; "Default value"

See also:

Catch and Throw
The catch and throw facility in Ruby is not really exception handling at all, though it does have similarities. As it shares keywords with Java exception handling, it seems to get lumped into any discussion on exceptions. This page is no different.

Calling a throw will interrupt the program flow, causing it to jump to the named catch. Use this format to set up a catch block. Any time throw :my_label is called within the block (including within methods called from the block).
catch :my_label do
throw :my_label
do_not_do this_stuff

You can use the throw to return a value to catch (which will be nil otherwise).
value = catch :my_label do
throw :my_label, "My result"
do_not_do this_stuff

Struggling with Ruby: Contents Page

1 comment:

Unknown said...

Finally a clear explanation. The pickaxe book is amazingly obtuse about this process. Thanks!