Programming Ruby
The Pragmatic Programmer's Guide
Exceptions, Catch, and Throw
So far we're been developing code in Pleasantville, a wonderful
place where nothing ever, ever goes wrong.
Every library call
succeeds, users never enter incorrect data, and resources are
plentiful and cheap. Well, that's about to change. Welcome to the
real world!
In the real world, errors happen. Good programs (and programmers)
anticipate them and arrange to handle them gracefully. This isn't
always as easy as it might be. Often the code that detects an error
does not have the context to know what to do about it. For example,
attempting to open a file that doesn't exist is acceptable in some
circumstances and is a fatal error at other times. What's your
file-handling module to do?
The traditional approach is to use return codes. The
open
method returns some specific value to say it failed. This value
is then propagated back through the layers of calling routines
until someone wants to take responsibility for it.
The problem with this approach is that managing all these error codes
can be a pain. If a function calls
open, then
read,
and finally
close, and each can return an error indication, how
can the function distinguish these error codes in the value it returns
to
its caller?
To a large extent, exceptions solve this problem. Exceptions let you
package up information about an error into an object. That exception
object is then propagated back up the calling stack automatically
until the runtime system finds code that explicitly declares that it
knows how to handle that type of exception.
The package that contains the information about an exception is an
object of class
Exception, or one of class
Exception's
children. Ruby predefines a tidy hierarchy of exceptions, shown in
Figure 8.1 on page 91. As we'll see later, this hierarchy
makes handling exceptions considerably easier.
Figure not available...
When you need to raise an exception, you can use one of the built-in
Exception classes, or you can create one of your own. If you
create your own, you might want to make it a subclass of
StandardError or one of its children. If you don't, your exception
won't be caught by default.
Every
Exception has associated with it a message string and a
stack backtrace. If you define your own exceptions, you can add
additional information.
Our jukebox downloads songs from the Internet using a TCP socket. The
basic code is simple:
opFile = File.open(opName, "w")
while data = socket.read(512)
opFile.write(data)
end
What happens if we get a fatal error halfway through the download? We
certainly don't want to store an incomplete song in the song list.
``I Did It My *click*''.
Let's add some exception handling code and see how it helps.
We
enclose the code that could raise an exception in a
begin/
end block and use
rescue clauses to tell Ruby the
types of exceptions we want to handle. In this case we're interested
in trapping
SystemCallError exceptions (and, by implication, any
exceptions that are subclasses of
SystemCallError), so that's what
appears on the
rescue line. In the error handling block, we
report the error, close and delete the output file, and then reraise
the exception.
opFile = File.open(opName, "w")
begin
# Exceptions raised by this code will
# be caught by the following rescue clause
while data = socket.read(512)
opFile.write(data)
end
rescue SystemCallError
$stderr.print "IO failed: " + $!
opFile.close
File.delete(opName)
raise
end
When an exception is raised, and independent of any subsequent
exception handling, Ruby places a reference to the
Exception
object associated with the exception in the global variable
$!
(the exclamation point presumably mirroring our surprise that any of
our code could cause errors). In the previous example, we used
this variable to format our error message.
After closing and deleting the file, we call
raise with no
parameters, which reraises the exception in
$!. This is a
useful technique, as it allows you to write code that filters
exceptions, passing on those you can't handle to higher levels. It's
almost like implementing an inheritance hierarchy for error
processing.
You can have multiple
rescue clauses in a
begin block, and
each
rescue clause can specify multiple exceptions to catch. At
the end of each rescue clause you can give Ruby the name of a local
variable to receive the matched exception. Many people find this more
readable than using
$! all over the place.
begin
eval string
rescue SyntaxError, NameError => boom
print "String doesn't compile: " + boom
rescue StandardError => bang
print "Error running script: " + bang
end
How does Ruby decide which rescue clause to execute? It turns out that
the processing is pretty similar to that used by the
case
statement. For each
rescue clause in the
begin block, Ruby
compares the raised exception against each of the parameters in turn.
If the raised exception matches a parameter, Ruby executes the body of
the
rescue and stops looking. The match is made using
$!.kind_of?(parameter), and so will succeed if the parameter
has the same class as the exception or is an ancestor of the
exception. If you write a
rescue clause with no parameter list,
the parameter defaults to
StandardError.
If no
rescue clause matches, or if an exception is raised outside
a
begin/
end block, Ruby moves up the stack
and looks for an
exception handler in the caller, then in the caller's caller, and so on.
Although the parameters to the
rescue clause are typically the
names of
Exception classes, they can actually be arbitrary
expressions (including method calls) that return an
Exception class.
Sometimes you need to guarantee that some processing is done at the
end of a block of code, regardless of whether an exception was raised.
For example, you may have a file open on entry to the block, and you
need to make sure it gets closed as the block exits.
The
ensure clause does just this.
ensure goes after the last
rescue clause and contains a chunk of code that will always be
executed as the block terminates. It doesn't matter if the block exits
normally, if it raises and rescues an exception, or if it is terminated
by an uncaught exception---the
ensure block will get run.
f = File.open("testfile")
begin
# .. process
rescue
# .. handle error
ensure
f.close unless f.nil?
end
The
else
clause is a similar, although less useful, construct. If
present, it goes after the
rescue clauses and before any
ensure. The body of an
else clause is executed only if no
exceptions are raised by the main body of code.
f = File.open("testfile")
begin
# .. process
rescue
# .. handle error
else
puts "Congratulations-- no errors!"
ensure
f.close unless f.nil?
end
Sometimes you may be able to correct the cause of an exception. In
those cases, you can use the
retry statement within a
rescue
clause to repeat the entire
begin/
end block.
Clearly there
is tremendous scope for infinite loops here, so this is a feature to
use with caution (and with a finger resting lightly on the interrupt
key).
As an example of code that retries on exceptions, have a look at the
following, adapted from Minero Aoki's
net/smtp.rb library.
@esmtp = true
begin
# First try an extended login. If it fails because the
# server doesn't support it, fall back to a normal login
if @esmtp then
@command.ehlo(helodom)
else
@command.helo(helodom)
end
rescue ProtocolError
if @esmtp then
@esmtp = false
retry
else
raise
end
end
This code tries first to connect to an SMTP server using the
EHLO
command, which is not universally supported. If the connection attempt
fails, the code sets the
@esmtp variable to
false and
retries the connection. If this fails again, the exception is reraised
up to the caller.
So far we've been on the defensive, handling exceptions raised by
others.
It's time to turn the tables and go on the offensive. (There
are those that say your gentle authors are always offensive, but
that's a different book.)
You can raise exceptions in your code with the
Kernel::raise
method.
raise
raise "bad mp3 encoding"
raise InterfaceException, "Keyboard failure", caller
The first form simply reraises the current exception (or a
RuntimeError if there is no current exception). This is used in
exception handlers that need to intercept an exception before passing
it on.
The second form creates a new
RuntimeError exception, setting its
message to the given string. This exception is then raised up the call
stack.
The third form uses the first argument to create an exception and then
sets the associated message to the second argument and the stack
trace to the third argument. Typically the first argument will be either the
name of a class in the
Exception hierarchy or a reference to an
object instance of one of these classes.
[Technically, this
argument can be any object that responds to the message
exception by returning an object such that
object.kind_of?(Exception) is true.] The stack trace is
normally produced using the
Kernel::caller
method.
Here are some typical examples of
raise in action.
raise
raise "Missing name" if name.nil?
if i >= myNames.size
raise IndexError, "#{i} >= size (#{myNames.size})"
end
raise ArgumentError, "Name too big", caller
In the last example, we remove the current routine from the stack
backtrace, which is often useful in library modules. We can take this
further: the following code removes two routines from the backtrace.
raise ArgumentError, "Name too big", caller[1..-1]
You can define your own exceptions to hold any information that you
need to pass out from the site of an error. For example, certain types
of network errors might be transient depending on the circumstances.
If such an error occurs, and the circumstances are right, you could
set a flag in the exception to tell the handler that it might be worth
retrying the operation.
class RetryException < RuntimeError
attr :okToRetry
def initialize(okToRetry)
@okToRetry = okToRetry
end
end
Somewhere down in the depths of the code, a transient error occurs.
def readData(socket)
data = socket.read(512)
if data.nil?
raise RetryException.new(true), "transient read error"
end
# .. normal processing
end
Higher up the call stack, we handle the exception.
begin
stuff = readData(socket)
# .. process stuff
rescue RetryException => detail
retry if detail.okToRetry
raise
end
While the exception mechanism of
raise and
rescue is great
for abandoning execution when things go wrong, it's sometimes nice to
be able to jump out of some deeply nested construct during normal
processing. This is where
catch and
throw come in handy.
catch (:done) do
while gets
throw :done unless fields = split(/\t/)
songList.add(Song.new(*fields))
end
songList.play
end
catch defines a block that is labeled with the given name
(which may be a
Symbol or a
String). The block is executed
normally until a
throw is encountered.
When Ruby encounters a
throw, it zips back up the call stack
looking for a
catch block with a matching symbol.
When it finds
it, Ruby unwinds the stack to that point and terminates the block. If
the
throw is called with the optional second parameter, that
value is returned as the value of the
catch. So, in the previous
example, if the input does not contain correctly formatted lines, the
throw will skip to the end of the corresponding
catch, not
only terminating the
while loop but also skipping the playing of
the song list.
The following example uses a
throw to terminate interaction with
the user if ``!'' is typed in response to any prompt.
def promptAndGet(prompt)
print prompt
res = readline.chomp
throw :quitRequested if res == "!"
return res
end
catch :quitRequested do
name = promptAndGet("Name: ")
age = promptAndGet("Age: ")
sex = promptAndGet("Sex: ")
# ..
# process information
end
As this example illustrates, the
throw does not have to appear within the
static scope of the
catch.
Extracted from the book "Programming Ruby -
The Pragmatic Programmer's Guide"
Copyright
©
2001 by Addison Wesley Longman, Inc. This material may
be distributed only subject to the terms and conditions set forth in
the Open Publication License, v1.0 or later (the latest version is
presently available at
http://www.opencontent.org/openpub/)).
Distribution of substantively modified versions of this document is
prohibited without the explicit permission of the copyright holder.
Distribution of the work or derivative of the work in any standard
(paper) book form is prohibited unless prior permission is obtained
from the copyright holder.