|
|||
Previous < |
Contents ^
|
Next >
|
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.
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... |
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.
opFile = File.open(opName, "w") while data = socket.read(512) opFile.write(data) end |
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 |
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 |
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.
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 |
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 |
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 |
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.
Kernel::raise
method.
raise raise "bad mp3 encoding" raise InterfaceException, "Keyboard failure", caller |
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 |
raise ArgumentError, "Name too big", caller[1..-1] |
class RetryException < RuntimeError attr :okToRetry def initialize(okToRetry) @okToRetry = okToRetry end end |
def readData(socket) data = socket.read(512) if data.nil? raise RetryException.new(true), "transient read error" end # .. normal processing end |
begin stuff = readData(socket) # .. process stuff rescue RetryException => detail retry if detail.okToRetry raise end |
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 |
throw
does not have to appear within the
static scope of the catch
.
Previous < |
Contents ^
|
Next >
|