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.