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 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 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
inmethodname
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.
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.
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.
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
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.
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.
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.
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.
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
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.
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.
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
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.
18.189.186.167