Exceptions and Exception Handling

An exception is an object that represents some kind of exceptional condition; it indicates that something has gone wrong. This could be a programming error—attempting to divide by zero, attempting to invoke a method on an object that does not define the method, or passing an invalid argument to a method. Or it could be the result from some kind of external condition—making a network request when the network is down, or trying to create an object when the system is out of memory.

When one of these errors or conditions occurs, an exception is raised (or thrown). By default, Ruby programs terminate when an exception occurs. But it is possible to declare exception handlers. An exception handler is a block of code that is executed if an exception occurs during the execution of some other block of code. In this sense, exceptions are a kind of control statement. Raising an exception transfers the flow-of-control to exception handling code. This is like using the break statement to exit from a loop. As we’ll see, though, exceptions are quite different from the break statement; they may transfer control out of many enclosing blocks and even up the call stack in order to reach the exception handler.

Ruby uses the Kernel method raise to raise exceptions, and uses a rescue clause to handle exceptions. Exceptions raised by raise are instances of the Exception class or one of its many subclasses. The throw and catch methods described earlier in this chapter are not intended to signal and handle exceptions, but a symbol thrown by throw propagates in the same way that an exception raised by raise does. Exception objects, exception propagation, the raise method, and the rescue clause are described in detail in the subsections that follow.

Exception Classes and Exception Objects

Exception objects are instances of the Exception class or one of its subclasses. Numerous subclasses exist. These subclasses do not typically define new methods or new behavior, but they allow exceptions to be categorized by type. The class hierarchy is illustrated in Figure 5-5.

Object
 +--Exception
     +--NoMemoryError
     +--ScriptError
     |   +--LoadError
     |   +--NotImplementedError
     |   +--SyntaxError
     +--SecurityError         # Was a StandardError in 1.8
     +--SignalException
     |   +--Interrupt
     +--SystemExit
     +--SystemStackError      # Was a StandardError in 1.8
     +--StandardError
         +--ArgumentError
         +--FiberError        # New in 1.9
         +--IOError
         |   +--EOFError
         +--IndexError
         |   +--KeyError      # New in 1.9
         |   +--StopIteration # New in 1.9
         +--LocalJumpError
         +--NameError
         |   +--NoMethodError
         +--RangeError
         |   +--FloatDomainError
         +--RegexpError
         +--RuntimeError
         +--SystemCallError
         +--ThreadError
         +--TypeError
         +--ZeroDivisionError

Figure 5-5. The Ruby Exception Class Hierarchy

You don’t need to be familiar with each of these exception subclasses. Their names tell you what they are used for. It is important to note that most of these subclasses extend a class known as StandardError. These are the “normal” exceptions that typical Ruby programs try to handle. The other exceptions represent lower-level, more serious, or less recoverable conditions, and normal Ruby programs do not typically attempt to handle them.

If you use ri to find documentation for these exception classes, you’ll find that most of them are undocumented. This is in part because most of them add no new methods to those defined by the base Exception class. The important thing to know about a given exception class is when it can be raised. This is typically documented by the methods that raise the exception rather than by the exception class itself.

The methods of exception objects

The Exception class defines two methods that return details about the exception. The message method returns a string that may provide human-readable details about what went wrong. If a Ruby program exits with an unhandled exception, this message will typically be displayed to the end user, but the primary purpose of this message is to aid a programmer in diagnosing the problem.

The other important method of exception objects is backtrace. This method returns an array of strings that represents the call stack at the point that the exception was raised. Each element of the array is a string of the form:

filename : linenumber in methodname

The first element of the array specifies the position at which the exception was raised; the second element specifies the position at which the method that raised the exception was called; the third element specifies the position at which that method was called; and so on. (The Kernel method caller returns a stack trace in this same format; you can try it out in irb.) Exception objects are typically created by the raise method. When this is done, the raise method sets the stack trace of the exception appropriately. If you create your own exception object, you can set the stack trace to whatever you want with the set_backtrace method.

Creating exception objects

Exception objects are typically created by the raise method, as we’ll see below. However, you can create your own objects with the normal new method, or with another class method named exception. Both accept a single optional string argument. If specified, the string becomes the value of the message method.

Defining new exception classes

If you are defining a module of Ruby code, it is often appropriate to define your own subclass of StandardError for exceptions that are specific to your module. This may be a trivial, one-line subclass:

class MyError < StandardError; end

Raising Exceptions with raise

The Kernel method raise raises an exception. fail is a synonym that is sometimes used when the expectation is that the exception will cause the program to exit. There are several ways to invoke raise:

  • If raise is called with no arguments, it creates a new RuntimeError object (with no message) and raises it. Or, if raise is used with no arguments inside a rescue clause, it simply re-raises the exception that was being handled.

  • If raise is called with a single Exception object as its argument, it raises that exception. Despite its simplicity, this is not actually a common way to use raise.

  • If raise is called with a single string argument, it creates a new RuntimeError exception object, with the specified string as its message, and raises that exception. This is a very common way to use raise.

  • If the first argument to raise is an object that has an exception method, then raise invokes that method and raises the Exception object that it returns. The Exception class defines an exception method, so you can specify the class object for any kind of exception as the first argument to raise.

    raise accepts a string as its optional second argument. If a string is specified, it is passed to the exception method of the first argument. This string is intended for use as the exception message.

    raise also accepts an optional third argument. An array of strings may be specified here, and they will be used as the backtrace for the exception object. If this third argument is not specified, raise sets the backtrace of the exception itself (using the Kernel method caller).

The following code defines a simple method that raises an exception if invoked with a parameter whose value is invalid:

def factorial(n)                 # Define a factorial method with argument n
  raise "bad argument" if n < 1  # Raise an exception for bad n
  return 1 if n == 1             # factorial(1) is 1
  n * factorial(n-1)             # Compute other factorials recursively
end

This method invokes raise with a single string argument. These are some equivalent ways to raise the same exception:

raise RuntimeError, "bad argument" if n < 1
raise RuntimeError.new("bad argument") if n < 1
raise RuntimeError.exception("bad argument") if n < 1

In this example, an exception of class ArgumentError is probably more appropriate than RuntimeError:

raise ArgumentError if n < 1

And a more detailed error message would be helpful:

raise ArgumentError, "Expected argument >= 1. Got #{n}" if n < 1

The intent of the exception we’re raising here is to point out a problem with the invocation of the factorial method, not with the code inside the method. The exception raised by the code here will have a backtrace whose first element identifies where raise was called. The second element of the array will actually identify the code that called factorial with the bad argument. If we want to point directly to the problem code, we can provide a custom stack trace as the third argument to raise with the Kernel method caller:

if n < 1
  raise ArgumentError, "Expected argument >= 1. Got #{n}", caller
end

Note that the factorial method checks whether its argument is in the correct range, but it does not check whether it is of the right type. We might add more careful error-checking by adding the following as the first line of the method:

raise TypeError, "Integer argument expected" if not n.is_a? Integer

On the other hand, notice what happens if we pass a string argument to the factorial method as it is written above. Ruby compares the argument n to the integer 1 with the < operator. If the argument is a string, the comparison makes no sense, and it fails by raising a TypeError. If the argument is an instance of some class that does not define the < operator, then we get a NoMethodError instead.

The point here is that exceptions can occur even if we do not call raise in our own code. It is important, therefore, to know how to handle exceptions, even if we never raise them ourselves. Handling exceptions is covered in the next section.

Handling Exceptions with rescue

raise is a Kernel method. A rescue clause, by contrast, is a fundamental part of the Ruby language. rescue is not a statement in its own right, but rather a clause that can be attached to other Ruby statements. Most commonly, a rescue clause is attached to a begin statement. The begin statement exists simply to delimit the block of code within which exceptions are to be handled. A begin statement with a rescue clause looks like this:

begin
  # Any number of Ruby statements go here.
  # Usually, they are executed without exceptions and
  # execution continues after the end statement.
rescue
  # This is the rescue clause; exception-handling code goes here.
  # If an exception is raised by the code above, or propagates up
  # from one of the methods called above, then execution jumps here.
end

Naming the exception object

In a rescue clause, the global variable $! refers to the Exception object that is being handled. The exclamation mark is a mnemonic: an exception is kind of like an exclamation. If your program includes the line:

require 'English'

then you can use the global variable $ERROR_INFO instead.

A better alternative to $! or $ERROR_INFO is to specify a variable name for the exception object in the rescue clause itself:

rescue => ex

The statements of this rescue clause can now use the variable ex to refer to the Exception object that describes the exception. For example:

begin                                # Handle exceptions in this block
  x = factorial(-1)                  # Note illegal argument
rescue => ex                         # Store exception in variable ex
  puts "#{ex.class}: #{ex.message}"  # Handle exception by printing message
end                                  # End the begin/rescue block

Note that a rescue clause does not define a new variable scope, and a variable named in the rescue clause is visible even after the end of the rescue clause. If you use a variable in a rescue clause, then an exception object may be visible after the rescue is complete, even when $! is no longer set.

Handling exceptions by type

The rescue clauses shown here handle any exception that is a StandardError (or subclass) and ignore any Exception object that is not a StandardError. If you want to handle nonstandard exceptions outside the StandardError hierarchy, or if you want to handle only specific types of exceptions, you must include one or more exception classes in the rescue clause. Here’s how you would write a rescue clause that would handle any kind of exception:

rescue Exception

Here’s how you would write a rescue clause to handle an ArgumentError and assign the exception object to the variable e:

rescue ArgumentError => e

Recall that the factorial method we defined earlier can raise ArgumentError or TypeError. Here’s how we would write a rescue clause to handle exceptions of either of these types and assign the exception object to the variable error:

rescue ArgumentError, TypeError => error

Here, finally, we see the syntax of the rescue clause at its most general. The rescue keyword is followed by zero or more comma-separated expressions, each of which must evaluate to a class object that represents the Exception class or a subclass. These expressions are optionally followed by => and a variable name.

Now suppose we want to handle both ArgumentError and TypeError, but we want to handle these two exceptions in different ways. We might use a case statement to run different code based on the class of the exception object. It is more elegant, however, to simply use multiple rescue clauses. A begin statement can have zero or more of them:

begin
  x = factorial(1)
rescue ArgumentError => ex
  puts "Try again with a value >= 1"
rescue TypeError => ex
  puts "Try again with an integer"
end

Note that the Ruby interpreter attempts to match exceptions to rescue clauses in the order they are written. Therefore, you should list your most specific exception subclasses first and follow these with more general types. If you want to handle EOFError differently than IOError, for example, be sure to put the rescue clause for EOFError first or the IOError code will handle it. If you want a “catch-all” rescue clause that handles any exception not handled by previous clauses, use rescue Exception as the last rescue clause.

Propagation of exceptions

Now that we’ve introduced rescue clauses, we can explain in more detail the propagation of exceptions. When an exception is raised, control is immediately transferred outward and upward until a suitable rescue clause is found to handle the exception. When the raise method executes, the Ruby interpreter looks to see whether the containing block has a rescue clause associated with it. If not (or if the rescue clause is not declared to handle that kind of exception), then the interpreter looks at the containing block of the containing block. If there is no suitable rescue clause anywhere in the method that called raise, then the method itself exits.

When a method exits because of an exception, it is not the same thing as a normal return. The method does not have a return value, and the exception object continues propagating from the site of the method invocation. The exception propagates outward through the enclosing blocks, looking for a rescue clause declared to handle it. And if no such clause is found, then this method returns to its caller. This continues up the call stack. If no exception handler is ever located, then the Ruby interpreter prints the exception message and backtrace and exits. For a concrete example, consider the following code:

def explode        # This method raises a RuntimeError 10% of the time
  raise "bam!" if rand(10) == 0
end

def risky   
  begin            # This block
    10.times do    # contains another block
      explode      # that might raise an exception.
    end            # No rescue clause here, so propagate out.
  rescue TypeError # This rescue clause cannot handle a RuntimeError..
    puts $!        # so skip it and propagate out.
  end              
  "hello"          # This is the normal return value, if no exception occurs.
end                # No rescue clause here, so propagate up to caller.

def defuse
  begin                     # The following code may fail with an exception.
    puts risky              # Try to invoke and print the return value.
  rescue RuntimeError => e  # If we get an exception
    puts e.message          # print the error message instead.
  end                       
end

defuse

An exception is raised in the method explode. That method has no rescue clause, so the exception propagates out to its caller, a method named risky. risky has a rescue clause, but it is only declared to handle TypeError exceptions, not RuntimeError exceptions. The exception propagates out through the lexical blocks of risky and then propagates up to the caller, a method named defuse. defuse has a rescue clause for RuntimeError exceptions, so control is transferred to this rescue clause and the exception stops propagating.

Note that this code includes the use of an iterator (the Integer.times method) with an associated block. For simplicity, we said that the exception simply propagated outward through this lexical block. The truth is that blocks behave more like method invocations for the purposes of exception propagation. The exception propagates from the block up to the iterator that invoked the block. Predefined looping iterators like Integer.times do no exception handling of their own, so the exception propagates up the call stack from the times iterator to the risky method that invoked it.

Exceptions during exception handling

If an exception occurs during the execution of a rescue clause, the exception that was originally being handled is discarded, and the new exception propagates from the point at which it was raised. Note that this new exception cannot be handled by rescue clauses that follow the one in which it occurred.

retry in a rescue clause

When the retry statement is used within a rescue clause, it reruns the block of code to which the rescue is attached. When an exception is caused by a transient failure, such as an overloaded server, it might make sense to handle the exception by simply trying again. Many other exceptions, however, reflect programming errors (TypeError, ZeroDivisionError) or nontransient failures (EOFError or NoMemoryError). retry is not a suitable handling technique for these exceptions.

Here is a simple example that uses retry in an attempt to wait for a network failure to be resolved. It tries to read the contents of a URL, and retries upon failure. It never tries more than four times in all, and it uses “exponential backoff” to increase the wait time between attempts:

require 'open-uri'

tries = 0       # How many times have we tried to read the URL
begin           # This is where a retry begins
  tries += 1    # Try to print out the contents of a URL
  open('http://www.example.com/') {|f| puts f.readlines }
rescue OpenURI::HTTPError => e  # If we get an HTTP error
  puts e.message                # Print the error message
  if (tries < 4)                # If we haven't tried 4 times yet...
    sleep(2**tries)             # Wait for 2, 4, or 8 seconds
    retry                       # And then try again!
  end
end

The else Clause

A begin statement may include an else clause after its rescue clauses. You might guess that the else clause is a catch-all rescue: that it handles any exception that does not match a previous rescue clause. This is not what else is for. The else clause is an alternative to the rescue clauses; it is used if none of the rescue clauses are needed. That is, the code in an else clause is executed if the code in the body of the begin statement runs to completion without exceptions.

Putting code in an else clause is a lot like simply tacking it on to the end of the begin clause. The only difference is that when you use an else clause, any exceptions raised by that clause are not handled by the rescue statements.

The use of an else clause is not particularly common in Ruby, but they can be stylistically useful to emphasize the difference between normal completion of a block of code and exceptional completion of a block of code.

Note that it does not make sense to use an else clause without one or more rescue clauses. The Ruby interpreter allows it but issues a warning. No rescue clause may appear after an else clause.

Finally, note that the code in an else clause is only executed if the code in the begin clause runs to completion and “falls off” the end. If an exception occurs, then the else clause will obviously not be executed. But break, return, next, and similar statements in the begin clause may also prevent the execution of the else clause.

The ensure Clause

A begin statement may have one final clause. The optional ensure clause, if it appears, must come after all rescue and else clauses. It may also be used by itself without any rescue or else clauses.

The ensure clause contains code that always runs, no matter what happens with the code following begin:

  • If that code runs to completion, then control jumps to the else clause—if there is one—and then to the ensure clause.

  • If the code executes a return statement, then the execution skips the else clause and jumps directly to the ensure clause before returning.

  • If the code following begin raises an exception, then control jumps to the appropriate rescue clause, and then to the ensure clause.

  • If there is no rescue clause, or if no rescue clause can handle the exception, then control jumps directly to the ensure clause. The code in the ensure clause is executed before the exception propagates out to containing blocks or up the call stack.

The purpose of the ensure clause is to ensure that housekeeping details such as closing files, disconnecting database connections, and committing or aborting transactions get taken care of. It is a powerful control structure, and you should use it whenever you allocate a resource (such as a file handle or database connection) to ensure that proper deallocation or cleanup occurs.

Note that ensure clauses complicate the propagation of exceptions. In our earlier explanation, we omitted any discussion of ensure clauses. When an exception propagates, it does not simply jump magically from the point where it is raised to the point where it is handled. There really is a propagation process. The Ruby interpreter searches out through containing blocks and up through the call stack. At each begin statement, it looks for a rescue clause that can handle the exception. And it looks for associated ensure clauses, and executes all of them that it passes through.

An ensure clause can cancel the propagation of an exception by initiating some other transfer of control. If an ensure clause raises a new exception, then that new exception propagates in place of the original. If an ensure clause includes a return statement, then exception propagation stops, and the containing method returns. Control statements such as break and next have similar effects: exception propagation is abandoned, and the specified control transfer takes place.

An ensure clause also complicates the idea of a method return value. Although ensure clauses are usually used to ensure that code will run even if an exception occurs, they also work to ensure that code will be run before a method returns. If the body of a begin statement includes a return statement, the code in the ensure clause will be run before the method can actually return to its caller. Furthermore, if an ensure clause contains a return statement of its own, it will change the return value of the method. The following code, for example, returns the value 2:

begin
  return 1     # Skip to the ensure clause before returning to caller
ensure
  return 2     # Replace the return value with this new value
end

Note that an ensure clause does not alter the return value of a method unless it explicitly uses a return statement. The following method, for example, returns 1, not 2:

def test
  begin return 1 ensure 2 end
end

If a begin statement does not propagate an exception, then the value of the statement is the value of the last expression evaluated in the begin, rescue, or else clauses. The code in the ensure clause is guaranteed to run, but it does not affect the value of the begin statement.

rescue with Method, Class, and Module Definitions

Throughout this discussion of exception handling, we have described the rescue, else, and ensure keywords as clauses of a begin statement. In fact, they can also be used as clauses of the def statement (defines a method), the class statement (defines a class), and the module statement (defines a module). Method definitions are covered in Chapter 6; class and module definitions are covered in Chapter 7.

The following code is a sketch of a method definition with rescue, else, and ensure clauses:

def method_name(x)
  # The body of the method goes here.
  # Usually, the method body runs to completion without exceptions
  # and returns to its caller normally.
rescue 
  # Exception-handling code goes here.
  # If an exception is raised within the body of the method, or if
  # one of the methods it calls raises an exception, then control
  # jumps to this block.
else
  # If no exceptions occur in the body of the method
  # then the code in this clause is executed.
ensure
  # The code in this clause is executed no matter what happens in the
  # body of the method. It is run if the method runs to completion, if 
  # it throws an exception, or if it executes a return statement.
end

rescue As a Statement Modifier

In addition to its use as a clause, rescue can also be used as a statement modifier. Any statement can be followed by the keyword rescue and another statement. If the first statement raises an exception, the second statement is executed instead. For example:

# Compute factorial of x, or use 0 if the method raises an exception
y = factorial(x) rescue 0

This is equivalent to:

y = begin
      factorial(x)
    rescue
      0
    end

The advantage of the statement modifier syntax is that the begin and end keywords are not required. When used in this way, rescue must be used alone, with no exception class names and no variable name. A rescue modifier handles any StandardError exception but does not handle exceptions of other types. Unlike if and while modifiers, the rescue modifier has higher precedence (see Table 4-2 in the previous chapter) than assignment operators. This means that it applies only to the righthand side of an assignment (like the example above) rather than to the assignment expression as a whole.

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

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