Chapter 9. Exception Handling

image with no caption

Even the most carefully written program will sometimes encounter unforeseen errors. For example, if you write a program that needs to read some data from disk, it works on the assumption that the specified disk is actually available and the data is valid. If your program does calculations based on user input, it works on the assumption that the input is suitable to be used in a calculation.

Although you may try to anticipate some potential problems before they arise—for example, by writing code to check that a file exists before reading data from it or checking that user input is numerical before doing a calculation—you will never be able to predict every possible problem in advance.

The user may remove a data disk after you’ve already started reading data from it, for example; or some obscure calculation may yield 0 just before your code attempts to divide by this value. When you know that there is the possibility that your code may be “broken” by some unforeseen circumstances at runtime, you can attempt to avoid disaster by using exception handling.

An exception is an error that is packaged into an object. The object is an instance of the Exception class (or one of its descendants). You can handle exceptions by trapping the Exception object, optionally using information that it contains (to print an appropriate error message, for instance) and taking any actions needed to recover from the error—perhaps by closing any files that are still open or assigning a sensible value to a variable that may have been assigned some nonsensical value as the result of an erroneous calculation.

rescue: Execute Code When Error Occurs

The basic syntax of exception handling can be summarized as follows:

begin
   # Some code which may cause an exception
rescue <Exception Class>
   # Code to recover from the exception
end

When an exception is unhandled, your program may crash, and Ruby is likely to display a relatively unfriendly error message:

div_by_zero.rb

x = 1/0
puts( x )

The program terminates with this error:

C:/bookofruby/ch9/div_by_zero.rb:3:in `/': divided by 0 (ZeroDivisionError)
 from C:/bookofruby/ch9/div_by_zero.rb:3:in `<main>'

To prevent this from happening, you should handle exceptions yourself. Here is an example of an exception handler that deals with an attempt to divide by zero:

exception1.rb

begin
   x = 1/0
rescue Exception
   x = 0
   puts( $!.class )
   puts( $! )
end
puts( x )

When this runs, the code following rescue Exception executes and displays this:

ZeroDivisionError
divided by 0
0

The code between begin and end is my exception-handling block. I’ve placed the troublesome code after begin. When an exception occurs, it is handled in the section beginning with rescue. The first thing I’ve done is to set the variable x to a meaningful value. Next come these two inscrutable statements:

puts( $!.class )
puts( $! )

In Ruby, $! is a global variable to which is assigned the last exception. Printing $!.class displays the class name, which here is ZeroDivisionError; printing the variable $! alone has the effect of displaying the error message contained by the Exception object, which here is “divided by 0.”

I am not generally keen on relying upon global variables, particularly when they have names as undescriptive as $!. Fortunately, there is an alternative. You can associate a variable name with the exception by placing the “assoc operator” (=>) after the class name of the exception and before the variable name:

exception2.rb

rescue Exception => exc

You can now use the variable name (here exc) to refer to the Exception object:

puts( exc.class )
puts( exc )

Although it may seem pretty obvious that when you divide by zero, you are going to get a ZeroDivisionError exception, in real-world code there may be times when the type of exception is not so predictable. Let’s suppose, for instance, that you have a method that does a division based on two values supplied by a user:

def calc( val1, val2 )
    return val1 / val2
end

This could potentially produce a variety of different exceptions. Obviously, if the second value entered by the user is 0, you will get a ZeroDivisionError.

exception_tree.rb

However, if the second value is a string, the exception will be a TypeError, whereas if the first value is a string, it will be a NoMethodError (because the String class does not define the “division operator,” which is /). Here the rescue block handles all possible exceptions:

multi_except.rb

def calc( val1, val2 )
    begin
        result = val1 / val2
    rescue Exception => e
        puts( e.class )
        puts( e )
        result = nil
    end
    return result
end

You can test this by deliberately generating different error conditions:

calc( 20, 0 )
      #=> ZeroDivisionError
      #=> divided by 0
calc( 20, "100" )
      #=> TypeError
      #=> String can't be coerced into Fixnum
calc( "100", 100 )
      #=> NoMethodError
      #=> undefined method `/' for "100":String

Often it will be useful to take different actions for different exceptions. You can do that by adding multiple rescue clauses. Each rescue clause can handle multiple exception types, with the exception class names separated by commas. Here my calc method handles TypeError and NoMethodError exceptions in one clause with a catchall Exception handler to deal with other exception types:

multi_except2.rb

def calc( val1, val2 )
    begin
        result = val1 / val2
    rescue TypeError, NoMethodError => e
        puts( e.class )
        puts( e )
        puts( "One of the values is not a number!" )
        result = nil
    rescue Exception => e
        puts( e.class )
        puts( e )
        result = nil
    end
    return result
end

This time, when a TypeError or NoMethodError is handled (but no other sort of error), my additional error message is displayed like this:

NoMethodError
undefined method `/' for "100":String
One of the values is not a number!

When handling multiple exception types, you should always put the rescue clauses dealing with specific exceptions first and then follow these with rescue clauses dealing with more generalized exceptions.

When a specific exception such as TypeError is handled, the begin..end exception block exits so the flow of execution won’t “trickle down” to more generalized rescue clauses. However, if you put a generalized exception-handling rescue clause first, that will handle all exceptions, so any more specific clauses lower down will never execute.

If, for example, I had reversed the order of the rescue clauses in my calc method, placing the generalized Exception handler first, this would match all exception types so the clause for the specific TypeError and NoMethodError exceptions would never be run:

multi_except_err.rb

# This is incorrect...
rescue Exception => e
      puts( e.class )
      result = nil
   rescue TypeError, NoMethodError => e
      puts( e.class )
      puts( e )
      puts( "Oops! This message will never be displayed!" )
      result = nil
   end
calc( 20, 0 )        #=> ZeroDivisionError
calc( 20, "100" )    #=> TypeError
calc( "100", 100 )   #=> NoMethodError
..................Content has been hidden....................

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