Chapter 7
Conditions
Exception Handling
A set of classes, functions, and associated conventions extend the underlying condition handling capabilities to provide a complete exception handling facility.
The classes are described in Conditions
on
page 244, and the functions are described
in "Signaling Conditions" on page
357.
Stack Model
Condition handlers are installed dynamically, with more recent handlers shadowing previously installed handlers. In addition, exception handling often involves the use of nonlocal exits. For these reasons it is useful to describe the behavior of the exception system using the following terms from the stack model of function calling.
- outside stack
The state existing just before the handler was established. - signaling unit
The conceptual program component that includes the expression that signaled the condition and does not include the expression that established the handler. This informal concept provides a notion of where the interface boundary between the signaler and the handler lies. - middle stack
The state existing just before the signaling unit was called, minus the outside stack. In other words, the state between the handler and the signaling unit. - inside stack
The state existing just before signaling occurred, minus the middle stack and outside stack. In other words, the portion of the signaling unit prior to the call tosignal
.
Figure 7-1 The Stack Model
The handler in Figure 7-1 may either return normally, in which
case execution resumes as the call to signal
returns normally, or the handler
may make a nonlocal exit, such as calling the exit function from a dynamically
active block
statement.
Recovery and Exits
There are two ways to handle an exception: by recovery or by exit. Recovery involves making some repair to the program state and leaving control in the signaling unit. Exit involves transferring control outside of the signaling unit through the use of a nonlocal exit.
The simplest way to handle an exception is to exit the signaling unit by taking a nonlocal
exit to a target established in the outside stack. The exception
clause of
the block
statement provides a convenient mechanism for
accomplishing this.
A less common handling style is to exit the signaling unit by taking a nonlocal exit to a target established in the middle stack, thus leaving the handler in force.
Instead of exiting, a handler can recover by returning control to the signaling unit. This can be done either by returning values that the signaling unit will understand or by taking a nonlocal exit to a target established in the inside stack.
The following examples show three ways of handling a copy-protection violation while copying a series of files. Note that the signaling code does not need to know how the condition will be handled. The only changes are in the code that handles the condition.
// Assume there is a class for file-system errors. // We are interested in a special kind of file-system error // that occurs when attempting to copy a copy-protected file, // so we define a new class to indicate such errors. define class <copy-protection-violation> (<file-system-error>) slot file, init-keyword: file:; // Store the file name end class; // Define a function to copy a single file. This // function signals a <copy-protection-violation> if // the file is copy-protected. define method copy-file (source, destination) if ( copy-protected?(source) ) signal(make(<copy-protection-violation>, file: source)); else // copy normally notify-user("Copying %s to %s.", source, destination); end if; end method; // The following function copies a sequence of files. // If one of the files is copy-protected, the user is // notified, and the remaining files are copied. define method backup-all-possible (volume, archive) let handler <copy-protection-violation> = method (condition, next) // The handler just notifies the user and continues notify-user("The file %s could not be copied.", condition.file); end method; // start copying files, with the handler in effect for (each-file in volume) copy-file(each-file, archive) end for; end method; // The following function stops copying as soon as it // hits a copy-protected file define method backup-exit (volume, archive) // set up a block so we can do a nonlocal exit block (exit) let handler <copy-protection-violation> = method (condition, next) // Notify the user and abort the backup notify-user( "Backup interrupted: the file %s could not be copied.", condition.file); exit(#f); end method; // start copying files, with the handler in effect for (each-file in volume) copy-file(each-file, archive) end for; end block; end method; // The following function uses the convenient exception clause of // the block statement to achieve essentially the same effect as // as backup-exit. define method backup-block (volume, archive) // get ready to do backups block () // start copying files for (each-file in volume) copy-file(each-file, archive) end for; exception (condition :: <copy-protection-violation>) notify-user( "Backup interrupted: the file %s could not be copied.", condition.file); end block; end method;
Restarts
Recovering or exiting can be accomplished directly, or a more formal mechanism called restarting can be used. Using restarts provides more assurance that the handler and the signaling unit agree on the meaning of what they are doing and provides some isolation of the handler from names and data representations internal to the signaling unit.
A handler restarts by signaling a restart. All restarts are instances
of <restart>
. Any values needed for recovery are passed
in the restart (that is, in initialization arguments that the restart remembers, typically
in slots). The restart is handled by a restart handler that
either returns or takes a nonlocal
exit. If the restart handler returns some values, signal
returns those values
and the handler that called signal
also returns them. The call
to signal
from the signaling unit that signaled the original condition returns
the same values, and the signaling unit recovers as directed by those
values.
Recovery Protocols
For every condition class there should be a recovery protocol that defines the meaning of handling by returning, the meaning of the values returned, and which restart handlers are supposed to be established by the signaling unit. The recovery protocol tells the handler what to expect from the signaler. For many condition classes, this is the empty protocol: handling by returning isn't allowed, and no particular restart handlers are provided. In this case only handling by exiting is possible. (Exiting might be accomplished by signaling a restart whose handler was established in the outside or middle stack and does a nonlocal exit back to where it was established, or by an ordinary nonlocal exit.) The recovery protocol for a subclass should be compatible with the recovery protocol of a superclass. That is, a handler that applies a class's recovery protocol should operate correctly when the condition is an instance of some subclass of that class.
An example recovery protocol for a hypothetical <unbound-slot>
condition
could include the following:
- Returning is allowed. Returning a value uses that value as if it had been the contents of the slot.
- A restart handler for
<new-value>
is available.<new-value>
has initialization argumentsvalue:
, the value to use, andpermanent:
, which indicates whether to store the value into the slot or leave the slot unbound.
No formal mechanism is provided for describing recovery protocols; they are left to the documentation of a condition class. Introspective functions are provided for discovering which recovery facilities are actually available, but this is different from (and sometimes is a superset of) the recovery facilities guaranteed by a recovery protocol always to be available.
The debugger is the condition handler of last resort. It
receives control if no program-provided handler handles a serious condition. (This is true
even if the debugger provided cannot analyze or intervene in the execution of programs but
can only abort or restart them. The debugger might be merely a core dumper,
a bomb
box,
or something similar.) An interactive debugger ought to offer the user the ability
to signal any restart for which a restart handler is applicable and to return if the
condition's recovery protocol allows it. This could, for example, be done with a menu
titled Recovery.