| |||
| 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 > |