4.4. Exception Handling

In the previous examples, it was casually mentioned how Ruby raises this or that type of error, if you perform an illicit operation. Just like C#, VB, and any other respectable modern programming language out there, Ruby offers full support for handling exceptions.

This is how you would typically "catch an exception" in C# and VB:


C#

int a = 5;
int b = 0;
int div;

try
{
    div = a / b;
    Console.WriteLine("This is never written");
}
catch (Exception ex)
{
    Console.WriteLine("Oops... {0}", ex.Message);
}


VB

Dim a As Integer = 5
Dim b As Integer = 0
Dim div As Integer

Try
    div = a / b
    Console.WriteLine("This is never written")
Catch ex As Exception
    Console.WriteLine("Oops... {0}", ex.Message)
End Try

In both cases a division by zero is attempted within the try statement, and the exception raised is caught by the catch clause, which prints "Oops... Attempted to divide by zero." in C#'s case and "Oops... Arithmetic operation resulted in an overflow." for VB. Ruby works in the same way through begin/rescue as shown here:

a = 5
b = 0

begin
  div = a /b
  puts "This is never written"
rescue Exception => ex
  puts "Oops... #{ex.message}"
end

The rescue clause sets the variable ex to reference an instance of the given error class. The Exception class is the root of all error classes in Ruby, and as such it catches any errors that can possibly be raised.

It is generally a bad idea to rescue all the exceptions in a single catch clause.

If you didn't specify Exception at all (that is, rescue => ex) this would still handle most errors in a program because the rescue clause would default ex to an instance of StandardError. There are, however, errors that are not subclasses of StandardError. For example, ScriptError (and its LoadError, NotImplementedError, and SyntaxError subclasses), NoMemoryError, and other low-level errors.

Please also keep in mind that there will be a few rearrangements to the exception hierarchy in the next version of Ruby (1.9), but this shouldn't really affect you too much.

Conversely, as long as you don't need a variable representing the just caught exception, you could remove the => ex part as well, reducing the basic begin/rescue statement to:

begin
  3/0
rescue
  puts "There was an error!"
end

Similarly to C# and VB, you can be more specific and only handle a certain type of exception as follows:

begin
  5.a_non_existing_method
  3/0
rescue ZeroDivisionError
  puts "You divided by zero!"
end

This snippet raises the following error:

errors.rb:2: undefined method 'a_non_existing_method' for 5:Fixnum (NoMethodError)

errors.rb is just the file that was running the snippet and the 2 afterwards indicates that the second line was the culprit behind the error. If you were to run the same from an irb session, you'd get an error message along the lines of:

NoMethodError: undefined method 'a_non_existing_method' for 5:Fixnum
         from (irb):2

Regardless of how you ran the snippet, the reason why Ruby raised an unhandled exception is that the rescue clause was ready to handle ZeroDivisionErrors, but not NoMethodErrors. Aside from providing you with a "catch-all" exception class, you can also chain multiple rescue clauses together to handle several exceptions differently, just like you would in C#, VB, and many other programming languages.

Ruby also supplies you with an else clause that is executed when no errors are raised in the begin/end block of code, and an ensure clause that is executed no matter what, for example:

begin
  # ... some error prone code
  # ...

rescue SystemCallError => ex
  puts "A system call failed: #{ex.message}"
rescue ZeroDivisionError
  puts "You divided by zero!"
rescue NoMethodError => ex
  puts "That method doesn't exist: #{ex.message}"
rescue Exception => ex
  puts "Error: #{ex.message}"
else
  puts "Yay! No errors!"
ensure
  puts "Error or not, this is always printed!"
end

ensure is, of course, the equivalent of finally in C# and VB.

As usual, Ruby also provides you with an inline option:

File.read('non_existent_file.txt') rescue puts "You need an existing file!"

When defining methods you can also skip begin and the final end:

def with
  begin
    #...
  rescue
    #...
  end
end

def without
  # ...
rescue
  # ...
end

4.4.1. Raising Errors

At times you may need to raise errors. In Ruby this is done through the Kernel#raise method. Try the following sessions in irb:

>> raise
RuntimeError:
        from (irb):1

As you can see, called on its own without arguments, it raises a RuntimeError with an empty message. Now add a message to the error, for example:

>> raise "A generic error message"
RuntimeError: A generic error message
        from (irb):2

Now you have a RuntimeError with a custom message. This works fine as long as the error that you want is a generic RuntimeError. But what if you want a ZeroDivisionError, ArgumentError, or ThreadError? You can pass the error type to rescue:

>> raise ZeroDivisionError, "Don't divide by 0, mkay?"
ZeroDivisionError: Don't divide by 0, mkay?
        from (irb):3

Perfect. Now you can use it to make your methods a little more robust as shown in Listing 4-4.

Example 4.4. Factorial Method That Can Raise an ArgumentError
def fact(n)
  if n >= 0
    (2..n).inject(1) {|f, x| f * x }
  else
    raise ArgumentError, "The factorial is defined for non-negative integers only."
  end
end

(0..10).each {|n| puts "#{n}:	#{fact(n)}" }

This prints the following table:

0:       1
1:       1
2:       2
3:       6
4:       24
5:       120
6:       720
7:       5040
8:       40320
9:       362880
10:      3628800

The method fact can now raise exceptions, so passing a negative number executes the else clause of the if statement within the method, therefore raising an ArgumentError.

If you place the following within the same file (fact2.rb):

puts fact(-5)

this prints to the standard error output stream (stderr):

fact2.rb:5:in 'fact': The factorial is defined for non-negative integers only.
(ArgumentError)
  from fact2.rb:10

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
13.59.32.1