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.

Figure 7-1 The Stack Model

Stack Model Diagram

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:

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.