Condition Handling in the Lisp Language Family

This paper appears in
Advances in Exception Handling Techniques,
edited by A. Romanovsky, C. Dony, J.L. Knudsen, and A. Tripathi.
This book, published in 2001, is part of Lecture Notes in Computer Science, Volume 2022,
published by Springer.

This paper was originally written as an HTML document, exactly as shown below. Any final reformatting that was done for hardcopy publication in LaTeX may have been lost in this version. Any new text that has been added for this annotated version appears bracketed and in color green; such text is intended to help clarify the historical context as time passes.
--Kent Pitman, 28-Feb-2002.

Annotated original document follows.
Click here for an index of other titles by Kent Pitman.


Condition Handling in the Lisp Language Family

by Kent M. Pitman

Copyright © 2001, Kent M. Pitman. All Rights Reserved.

[ Web version Copyright © 2002, Kent M. Pitman. All Rights Reserved. ]

Introduction

The Lisp family of languages has long been a rich source of ideas and inspiration in the area of error handling. Here we will survey some of the abstract concepts and terminology, as well as some specific language constructs that Lisp has contributed.

Although there are numerous dialects of Lisp, several of which offer the modern concepts and capabilities described herein, we will focus specifically on Common Lisp as described in the ANSI standard, X3.226-1994 [X3J13 1994].

Condition Systems vs Error Systems

The Common Lisp community typically prefers to speak about its condition system rather than its error system to emphasize that there are not just fatal but also non-fatal situations in which the capabilities provided by this system are useful.

Not all exceptional situations are represented, or sometimes even detected. A situation that is represented within the language is referred to in Common Lisp as a condition; an object of class CONDITION is used to represent such a situation.

A condition is said to be the generalization of an error. Correspondingly, within the language the class CONDITION is a superclass of another class ERROR, which represents situations that would be fatal if not appropriately managed.

So the set of all situations involving conditions includes not only descriptions of outright erroneous situations but also descriptions of situations that are merely unusual or questionable. Even in the case of non-error conditions, the programmer may, as a matter of expressive freedom, choose to use the same capabilities and protocols as would be used for "real" error handling.

Condition Handling Is Primarily a Protocol Matter

To properly understand condition handling, it is critical to understand that it is primarily about protocol, rather than mere computational ability. The establishment of protocols is a sort of before-the-fact hedge against the "prisoner's dilemma"; that is, it creates an obvious way for two people who are not directly communicating to structure independently developed code so that it works in a manner that remains coherent when such code is later combined.

For example, if we want to write a program that searches a list for an object, returning true if the object is present and false otherwise, we could write the following, but would ordinarily not:

 (defun search-list (goal-item list-to-search)
   (handler-case 
       ;; Main body
       (progn (dolist (item list-to-search) 
                (when (eq item goal-item)
                  (return-from search-list t)))
              ;; Search has failed, signal an error.
              (error 'search-failure 
                     :item goal-item))
     ;; Upon error, just return false.
     (error () nil)))

The reason not to write this is not that it will not work, but that it involves undue amounts of unneeded mechanism. The language already contains simpler ways of expressing transfer of control from point A to point B in a program where the same programmer, acting in the same role, controls the code at both points. For example, the following would suffice:

 (defun search-list (goal-item list-to-search)
   (dolist (item list-to-search) 
     (when (eq item goal-item)
       (return-from search-list t)))
   nil)

However, if the intended action in the case of a failing search were not specified, and was to be provided by the caller, the use of condition handling mechanisms might be appropriate. For example, in the following code, only the signaling is occurring, and the handling is being left to the caller. Because of this, the use of condition handling facilities is appropriate because those facilities will provide matchmaking and data transport services between the signaler and the handler.

 (defun search-list (goal-item list-to-search)
   (dolist (item list-to-search) 
     (when (eq item goal-item)
       (return-from search-list t)))
   (error 'search-failure :item goal-item))

Protocol is simply not needed when communicating lexically among parts of a program that were written as a unit and that are not called by other programs that are either logically separated or, at minimum, logically separable. The distinction is subjective, but it is nevertheless important.

Historical Influences

Before beginning to look in detail at the features of Common Lisp's condition system, it's useful to observe that computer languages, like human languages, evolve over time both to accommodate current needs and to repair problems observed in past experience. The interesting features of the Common Lisp condition system were not suddenly designed one day as a spontaneous creative act, but rather grew from many years of prior experience in other languages and systems.

Influence of Multics PL/I on Symbolics Zetalisp

The PL/I language, designed at IBM in the early 1960's, included an extensive condition mechanism which had an extensible set of named signals and dynamic handlers running in the dynamic context of the signal. Since PL/I included "downward" lexical closures these handlers had access to the erring environment, and sometimes to the details of the error.

Multics, whose official language was PL/I, adapted and extended this, including the addition of any_other, cleanup, and unclaimed_signal, the passing of machine conditions and other arbitrary data, cross-protection-domain signals, and the use of this mechanism to manage multiple suspended environments on one stack.

For security reasons, Multics had an elaborate and rigidly enforced separation of various kinds of code into protected "rings". The Multics operating system relied on their expanded signaling system for several critical system functions, mostly in the user ring, although cross-ring signals were possible in certain cases involving paging or memory errors.

Historically, Multics was an "early" environment, but it was not, by any analysis, a toy. Its condition system was carefully designed, heavily tested, and had many important characteristics that influenced the later design of Lisp:

In the early 1980's, some former users of the Multics system, including Daniel L. Weinreb, Bernard Greenberg and David Moon, harvested the good ideas of the Multics PL/I condition system and recast them into Zetalisp, a dialect running on the Symbolics Lisp Machines. This redesign was called simply the "New Error System" (or sometimes just "NES").

Influence of Symbolics Zetalisp on Common Lisp

Key elements of the New Error System (NES) in Symbolics Zetalisp were:

NES directly and strongly influenced the design of the Common Lisp condition system. In fact, one initial concern voiced by a number of vendors was that they were fearful that somehow the ideas of the condition system, being taken from the Lisp Machine environment, would not perform well on standard hardware. It took several months of discussion, and the availability of a free public implementation of the ideas, before these fears were calmed and the Common Lisp community was able to adopt them. Even so, numerous small changes and a few major changes were made in the process.

In both Zetalisp and Common Lisp, handlers are functions that are called in the dynamic context of the signaling operation. No stack unwinding has yet occurred when the handlers are called. Potential handlers are tried in order until one decides to handle the condition. Probably the most conspicuous change between NES and the Common Lisp Condition System was the choice of how a handler function communicated its decision to elect a specific mode of recovery for the condition being signaled.

NES used a passive recovery mechanism. That is, in all cases, the handler would return one or more values. The nature of the return values would determine which recovery mode (called a "proceed type" in Zetalisp) was to be used. If NIL was returned, the handler had elected no proceed type, and the next handler was tried. Otherwise, the handler must return at least one value, a keyword designating the proceed type, and, optionally, additional values which were data appropriate to that manner of proceeding.

Common Lisp uses an active recovery mechanism. That is, any handler that wishes to designate a recovery mechanism (called a "restart" in Common Lisp) must imperatively transfer control to that restart. If the handler does not transfer control, that is, if the handler returns normally, any returned values are ignored and the handler is said to have "declined" (i.e., elected no restart), and the next handler is tried.

The Maclisp Experience

I am commonly credited with the "creation" of the Common Lisp Condition System, although I hope to show through this paper that my role in the design was largely to take the ideas of others and carefully transplant them to Common Lisp. In doing this, I relied on my personal experiences to guide me, and many of my formative experiences came from my work with Maclisp [Pitman 1983], which originated at MIT's Project MAC (later renamed to be the Laboratory for Computer Science), and which ran on the Digital Equipment Corporation (DEC) PDP10, DEC TOPS20 and Honeywell Multics systems.

Maclisp, had a relatively primitive error system, which I had used extensively. At the time I came to the Lisp Machine's NES, I did not know what I was looking for in an error system, but I knew, based on my experience with Maclisp, what I was not looking for. So what impressed me initially about NES was that it had fixed many of the design misfeatures that I had seen in Maclisp.

One important bit of background on Maclisp, at least on the PDP10 implementation that I used, was that it had no STRING datatype. In almost all cases where one might expect strings to be used, interned symbols were used instead. Symbols containing characters that might otherwise confuse the tokenizer were bounded on either end by a vertical bar (|). Also, since symbols would normally name variables, they generally had to be quoted with a leading single quote (') to protect them from the Lisp evaluation mechanism and allow them to be used as pseudostrings.

 '|This is a quoted Maclisp symbol.|

Poor Separation of Signaling and Handling in Maclisp

Maclisp had two forms of the function ERROR. In the simple and most widely used form, one merely called ERROR with one argument, a description of the error. Such errors would stop program execution with no chance of recovery other than to transfer to the innermost ERRSET, the approximate Maclisp equivalent of Common Lisp's IGNORE-ERRORS.

 (error '|YOU LOSE|)

It was possible, however, in a limited way, to specify the particular kind of error. There were about a dozen predefined kinds of errors that one could identify that did allow recovery. For example,

 (error '|VARIABLE HAS NO VALUE| 'A 'UNBND-VRBL)

The "keyword" UNBND-VRBL was a system-defined name that indicated to the system that this was an error of kind "unbound variable". A specific recovery strategy was permitted in this case. One could, either interactively in a breakpoint or through the dynamic establishment of a handler for such errors, provide a value for the variable. If that happened, the call to ERROR would then return a list of that value and the caller of ERROR was expected to pick up that value and use it.

This worked fine for the case where the programmer knew the kind of error and was prepared to recover from it. But a strange situation occurred when one knew the kind of error but was not prepared to recover. Sometimes one knew one had an unbound variable, and wanted to call ERROR, but was not prepared to recover. In this case, the programmer was forced to lie and to say that it was an error of arbitrary type, using just the short form, to avoid the misperception on the part of potential handlers that returning a recovery value would be useful.

 (error '|VARIABLE HAS NO VALUE| 'A)

One feature of the NES, which I personally found very attractive, was the notion that I could freely specify the class of error without regard to whether I was prepared to handle it in some particular way. The issue of how to handle the error was specified orthogonally.

Error Message Identity In Maclisp

In PDP10 Maclisp, error messages were historically all uppercase, since the system's primitive error messages were that way and many users found it aesthetically unpleasant to have some messages in mixed case while others were entirely uppercase. At some point, however, there was pressure to provide mixed case error messages. The decision made by the Maclisp maintainers of the time was not to yield to such pressure.

The problem was that many programs faced with an error message were testing it for object identity. For example:

 (eq msg '|UNBOUND VARIABLE|)

Had we changed the case of all of the error messages in the Maclisp system to any other case, lower or mixed, these tests would have immediately begun to fail, breaking a lot of installed code and costing a lot of money to fix. The change would have been seen to be gratuitous.

The lesson from this for all of us in the Maclisp community, which became magnified later when we confronted the broader community of international users, was that the identity of an error should not be its name. That is, had we to do it over again, we would not have used |unbound variable| nor |Unbound Variable| as the identity of the error, but rather would have created objects whose slots or methods were responsible for yielding the presented string, but whose identity and nature was controlled orthogonally. This was another thing that NES offered that drew me immediately to it.

Terminological Influences

At the time of the Common Lisp design, Scheme did not have an error system, and so its contribution to the dialog on condition systems was not that of contributing an operator or behavior. However, it still did have something to contribute: the useful term continuation. For our purposes here, it is sufficient to see a continuation as an actual or conceptual function that represents, in essence, one of possibly several "future worlds", any of which can be entered by electing to call its associated continuation.

This metaphor was of tremendous value to me socially in my efforts to gain acceptance of the condition system, because it allowed a convenient, terse explanation of what "restarts" were about in Common Lisp. Although Scheme continuations are, by tradition, typically passed by explicit data flow, this is not a requirement. And so I have often found myself thankful for the availability of a concept so that I could talk about the establishment of named restart points as "taking a continuation, labeling it with a tag, and storing it away on a shelf somewhere for possible later use."

Likewise, I found it useful in some circles to refer to some of the concepts of reflective Lisps, such as Brian Smith's "3Lisp" [Smith 1982], and later work inspired by it. I feel that the condition system's ability to introspect (through operators such as FIND-RESTART) about what possible actions are pending, without actually invoking those pending actions, is an important reflective capability. Even though Common Lisp does not offer general-purpose reflection, the ability to use this metaphor for speaking about those aspects of the language that are usefully described by it simplifies conversations.

Abstract Concepts

Having now hopefully firmly established that the formative ideas in the Common Lisp Condition System did not all spring into existence with the language itself, and are really the legacy of the community using the continuum of languages of which Common Lisp is a part, we can now turn our attention to a survey of some of the important features that Common Lisp provides.

Separating Signaling and Handling

Traditionally, "error handling" has been largely a process of programs stopping and the only real question has been "how much of the program stops?" or "how far out do I throw?" It is against that backdrop that modern condition handling can be best understood.

The proper way to think about condition handling is this:

The process of programming is about saying what to do in every circumstance. In that regard, a computer has been sometimes characterized as a "relentless judge of incompleteness". When a program reaches a place where there are several possible next steps and the program is unwilling or incapable of choosing among them, the program has detected an exceptional situation.

The possible next steps are called restarts. Restarts are, effectively, named continuations.

The process of asking for help in resolving the problem of selecting among the possible next steps is called signaling.

The independently contributed pieces of code which are consulted during the signaling process are called handlers. In Common Lisp, these are functions contributed by the dynamic call chain that are tried in order from innermost (i.e., most specific) to outermost (i.e., most general). Each handler is called with an argument that is a description of the problem situation. The handler will transfer control (by GO, RETURN or THROW) if it chooses to handle the problem described by its argument.

In describing condition handling, I tell the following story to help people visualize the need for its various parts:

Think of the process of signaling and handling as analogous to finding a fork in a road that you do not commonly travel. You don't know which way to go, so you make known your dilemma, that is, you signal a condition. Various sources of wisdom (handlers) present themselves, and you consult each, placing your trust in them because you have no special knowledge yourself of what to do. Not all sources of wisdom are experts on every topics, so some may decline to help before you find one that is confident of its advice. When an appropriately confident source of wisdom is found, it will act on your behalf. The situation has been handled.

In the case that the situation is not handled, the next action depends on which operator was used to signal. The function signal will just return normally when a condition goes unhandled. The function error is like signal, but rather than return, it enters the debugger. The Common Lisp debugger might allow access to low-level debugging features such as examination of individual storage locations, but it is not required to. Its primary role is to be an interactive handler; that is, to present the human user interactively with various options about how computation might be resumed. Conceptually, this is the same as if it were acting as the human user's proxy in being the element on the list of handlers, so that the human user is the source of wisdom whose choice will determine how to proceed. Other capabilities that the debugger might offer in support of that human's decision are probably very important in practice, but are conceptually uninteresting to this understanding of the debugger's role in signaling and handling.

Note, too, that in some possible future world, knowledge representation may have advanced enough that handlers could, rather than act unconditionally on behalf of the signaler, merely return a representation of a set of potential actions accompanied by descriptive information respresenting motivations, consequences, and even qualitative representations of the goodness of each. Such information might be combined with, compared to, or confirmed by recommendations from other sources of wisdom in order to produce a better result. This is how consultation of sources of wisdom would probably work in the real world. Consider that even a doctor who is sure of what a patient needs will ask the patient's permission before acting. However, this last step of confirmation, which would allow more flexibility in the reasoning process, is not manifest in Common Lisp as of the time of writing this paper. It is an open area for future research.

Some of these issues are discussed in much greater detail in my 1990 conference paper [Pitman 1990].

Generalized Conditions

It was mentioned earlier that the space of conditions that can be used in the Common Lisp Condition System is more general than the space of mere errors. Here are some examples.

Serious, non-error Conditions

The superclass of error is serious-condition. This kind of condition is a subclass of condition but is serious enough that conditions of this kind should generally enter the debugger if unhandled. Serious conditions, which the Zetalisp NES called "debugger conditions", exist as a separately named concept from "error conditions" to accommodate things that are not semantic errors in a program, but are instead resource limitations and other incidental accomodations to pragmatics.

Suppose one writes the following:

 (ignore-errors (open "some.file"))

This will trap errors during the file open. However, what if a stack overflow occurs, not for reasons of infinite recursion, but merely because the call is nested very deeply in other code? The answer is that a stack overflow is considered a serious condition, but not an error. The above code is equivalent to:

 (handler-case (open "some.file")
   (error (c)
     (values nil c)))

And since any condition representing a stack overflow is going to be a kind of SERIOUS-CONDITION, but not a kind of ERROR, the use of IGNORE-ERRORS will succeed in trapping a file error but not a stack overflow. If one wanted to catch serious conditions as well, one would write instead:

 (handler-case (open "some.file")
   (serious-condition (c)
     (values nil c)))

Non-serious conditions

Some conditions are not at all serious. Such conditions might be handled, but there is an obvious default action in the case of their going unhandled.

Consider a program doing line-at-a-time output to a console. One might assume the screen to have infinite height, and the output might look like:

 (defvar *line-number* 0)
 (defun show-lines (lines)
   (dolist (line lines)
     (show-line line)))

However, it might be useful to specify screen line height, and to have the console pause every so many lines for a human reader to confirm that it's ok to proceed. There are, of course, a number of ways such a facility could be programmed, but one possible such way is to use the condition system. For example,

 (defvar *line-number* 0)
 (defvar *page-height* nil)
 (define-condition end-of-page (condition) ())
 (defun show-lines (lines)
   (dolist (line lines)
     (show-line line)  ; Oops. Omitted this call from original paper!
     (incf *line-number*)
     (when (and *page-height*
                (zerop (mod *line-number* *page-height*)))
       (restart-case (signal 'end-of-page)
         (continue ())))))

In the above, there is only one way to proceed. A restart named CONTINUE is offered as a way of imperatively selecting this option (imperatively bypassing any other pending handlers), but if the handler declines and the condition goes unhandled, the same result will be achieved.

A similar kind of facility could be used to manage end of line handling. There, it's common to allow various modes, and so a corresponding set of restarts has to be established, which handlers would choose among. If no handler elected to handle the condition, however, no great harm would come. Here's an example of how that might look:

 (defvar *line-length* nil)
 (define-condition end-of-line (condition) ())
 (defun show-line (line)
   (let ((eol (or *line-length* -1)) (hpos 0))
     (loop for pos below (length line)
           for ch = (char line pos)
           do (write-char ch)
           when (= hpos eol)
             do (restart-case (signal 'end-of-line)
                  (wrap ()
                    (write-char #\Newline)
                    (setq hpos 0))
                  (truncate ()
                    (return))
                  (continue ()
                    ;; just allow to continue
                    ))
           else do (incf hpos))))

Independent, Reflective Specification of Restarts

It has long been the case that Lisp offered the ability to dynamically make the decision to transfer to a computed return point using the special operator THROW. However, without reflective capabilities, there has not been the ability for a programmer to determine if there was a pending CATCH to which control could be thrown other than relatively clumsy idioms such as the following:

 (ignore-errors (throw 'some-tag 'some-value))

The problem with the above idiom is that while it "detects" the presence or absence of a pending tag, it only retains local control and the ability to reason about this knowledge in the case of the tag's non-existence. The price of detecting the tag's existence is transfer to that tag.

The Common Lisp Condition System adds a limited kind of reflective capability in the form of a new kind of catch point, called a restart, the presence or absence of which can be reasoned about without any attempt to actually perform a transfer. A restart can also have associated with it a descriptive textual string that a human user can be shown by the debugger to describe the potential action offered by the restart.

Restart points that require transfer of control but no data can be established straightforwardly with WITH-SIMPLE-RESTART. For example:

 (defun lisp-top-level-loop ()
   (with-simple-restart (exit "Exit from Lisp.")
     (loop
       (with-simple-restart (continue "Return to Lisp toplevel.")
         (print (eval (read)))))))

Restarts that require data can also be established using a slightly more elaborate syntax. This syntax not only accommodates the programmatic data flow to the restart, but also enough information for the Common Lisp function INVOKE-RESTART-INTERACTIVELY to properly prompt for any appropriate values to be supplied to that restart. For example:

 (defun my-symbol-value (name)
   (if (boundp name)
       (symbol-value name)
     (restart-case (error 'unbound-variable :name name)
       (use-value (value)
         :report "Specify a value to use."
         :interactive (lambda ()
                        (format t "~&Value to use: ")
                        (list (eval (read))))
         value)
       (store-value (value)
         :report "Specify a value to use and store."
         :interactive (lambda ()
                        (format t "~&Value to use and store: ")
                        (list (eval (read))))
         (setf (symbol-value name) value)
         value))))

Code that inquires about such restarts typically makes use of FIND-RESTART to test for the availability of a restart, and then INVOKE-RESTART to invoke a restart. For example:

 (handler-bind ((unbound-variable
                  #'(lambda (c) ;argument is condition description
                      ;; Try to make unbound variables get value 17
                      (dolist (tag '(store-value use-value))
                        (let ((restart (find-restart tag c)))
                          (when restart
                            (invoke-restart restart 17)))))))
   (+ (my-symbol-value 'this-symbol-has-no-value) 
      (my-symbol-value 'pi))) ;pi DOES have a value!
 => 20.141592653589793

Absent such a handler, the restart would be offered interactively by the debugger, as in:

 (+ (my-symbol-value 'this-symbol-has-no-value) 
    (my-symbol-value 'pi))
 Error: The variable THIS-SYMBOL-HAS-NO-VALUE is unbound.
 Please select a restart option:
   1 - Specify a value to use.
   2 - Specify a value to use and store.
   3 - Return to Lisp toplevel.
   4 - Exit from Lisp.
 Option: 1
 Value to use: 19
 => 22.141592653589793

Handling in the Context of the Signaler

A key capability provided by Common Lisp is the fact that, at the most primitive level, handling can be done in the dynamic context of the signaler, while certain very critical dynamic state information is still available that would be lost if a stack unwind happened before running the handler.

This capability is reflected in the ability of the operator handler-bind to take control of a computation before any transfer of control occurs. Note that the Common Lisp operator handler-case, which is more analogous to facilities offered in other languages, does not allow programmer-supplied code to run until after the transfer of control; this is useful for some simple situations, but is less powerful.

Consider a code fragment such as:

 (handler-case (main-action)
   (error (c) (other-action)))

In this example, the expression (other-action) will run after unwinding from wherever in (main-action) signaled the error, regardless of how deep into main-action that signaling occured.

By contrast, handler-bind takes control inside the dynamic context of the call to SIGNAL, and so is capable of accessing restarts that are dynamically between the call to SIGNAL and the use of HANDLER-BIND. Consider this example:

 (with-simple-restart (foo "Outer foo.")
   (handler-case (with-simple-restart (foo "Inner foo.")
                   (error "Lossage."))
      (error (c) (invoke-restart 'foo))))

In the above, the outer FOO restart will be selected, as contrasted with the following, where the inner FOO restart will be selected:

 (with-simple-restart (foo "Outer foo.")
   (handler-bind ((error #'(lambda (c) (invoke-restart 'foo))))
     (with-simple-restart (foo "Inner foo.")
       (error "Lossage."))))

This is important because error handling tends to want to make use of all available restarts, but especially those that are in that code region that HANDLER-BIND can see but HANDLER-CASE cannot. Consider another example:

 (handler-case (foo)
   (unbound-variable (c)
     (let ((r (find-restart 'use-value c))) 
       (if r (invoke-restart r nil)))))

The above example will not achieve its presumed intent, which is to supply NIL as the default value for any unbound variable encountered during the call to FOO. The problem is that any USE-VALUE restart that is likely to be found will also be within the call to FOO, and will no longer be active by the time the ERROR clause of the HANDLER-CASE expression is executed.

Use of HANDLER-BIND allows this example to work in a way that is not possible with HANDLER-CASE and its analogs in other programming languages. Consider:

 (handler-bind (error
                 #'(lambda (c)
                     (let ((r (find-restart 'use-value c))) 
                       (if r (invoke-restart r nil)))))
   (foo))

Default Handling

Zetalisp contained a facility not only for asserting handlers to be used for conditions, but also an additional facility for asserting handlers that should be provisionally used only if no normal handlers were found. In effect, this meant there were two search lists, a handlers list and a default handlers list, which were searched in order.

In my use of Zetalisp's NES, I became convinced that it was conceptually incorrect to search the default handlers list in order; I felt it should be searched in reverse order. I had reported this as a bug, but it was never fixed. In all honesty, I'm not sure there was enough data then or perhaps even now to say whether I was right, although I continue to believe that default handling is something that should proceed from the outside in, not the inside out. Nevertheless, whether I was right or not is not so much relevant in this context as is the fact that it was a point of controversy that ended up influencing the design of Common Lisp's condition system. I was distrustful of the operator condition-bind-default that was offered by NES, and so I omitted it from the set of offerings that I transplanted to Common Lisp.

The Common Lisp Condition System does provide a way to implement the concept of a default handler, but it is idiomatic. And, perhaps not entirely coincidentally, it has the net effect of seeking default handlers from the outside in rather than the inside out, as I had always felt was right.

The Zetalisp mechanism looked like this:

(condition-bind-default ((error
                            #'(lambda (c)
                                ...default handling...)))
  ...body in which handler is in effect...)

The corresponding Common Lisp idiom looks like this:

(handler-bind ((error
                 #'(lambda (c)
                     (signal c) ;resignal
                     ...default handling...)))
  ...body in which handler is in effect...)

In effect, the Common Lisp idiom continues the signaling process but without explicitly relinquishing control. If the resignaled condition is unhandled, control will return to this handler and the default handling will be done. If, on the other hand, some outer handler does handle the condition, the default handling code will never be reached and so will not be run.

Unifying "Signals" and "Exceptions"

In some systems, such as Unix, "signals" are an asynchronous mechanism primarily used for implementing event-driven programming interfaces, but are not generally used within ordinary, synchronous programming.

While it is beyond the scope of the ANSI Common Lisp standard to address the issue of either interrupts or multitasking, most Common Lisp implementations have a convergent manner of coping with these issues that is sufficiently stable as to be worth some mention. The approach has been to separate the notion of "interrupting" from the notion of "signaling".

That is, in Common Lisp, all signaling is synchronous. However, such synchronous behavior can be usefully coupled with a process interruption to produce interesting effects.

In this separation, process interruptions without signaling might be done for any reason that involved the need to read or modify dynamic state of another process. Here is an example that merely reads the dynamic state of another process:

 (defvar *my-dynamic-variable* 1)

 (let ((temporary-process
          (mp:process-run-function "temp" '()
             ;; Launch a temporary process that
             ;;  merely dynamically binds a 
             ;;  certain variable and then
             ;;  sleeps for a minute.
             #'(lambda ()
                 (let ((*my-dynamic-variable* 2)) 
                   (sleep 60)))))
       (result-value nil))
   ;; Now interrupt our temporary process 
   ;; to see the value of the variable
   (mp:process-interrupt temporary-process
     #'(lambda () 
         (setq result-value *my-dynamic-variable*)))
   ;; Now wait for the interrupt to occur
   (mp:process-wait "not yet assigned"
     #'(lambda () result-value)) ;tests for a non-null value
   ;; If we get this far, the result-value has been assigned
   ;; and can be returned.
   result-value)

 => 2

Note that this merely examines the dynamic state of our temporary process, but does not invoke any signaling mechanism at all. And while the process of interruption is inherently asynchronous, the actions to be done in the interrupted process are synchronous.

If we instead intertwine the notion of process interruption with signaling, we get what some systems might call "asynchronous signaling", but which Common Lisp views as just the composition of two orthogonal facilities. So, for example, a keyboard interrupt to a process might be accomplished by:

 (define-condition keyboard-interrupt ()
   ((character :initarg :character :reader kbd-char))
   (:report (lambda (condition stream)   
              (format t "The character ~@:C was typed."
                      (kbd-char condition)))))

 (defun keyboard-interrupt (process character)
   (mp:process-interrupt process
      #'(lambda ()
          ;; Offer the process a chance to handle the condition.
          ;; If the condition is not handled, the call to SIGNAL returns
          ;; and the interrupt is completed.  Normal process execution
          ;; then continues.
          (signal 'keyboard-interrupt :character character))))

Using such a facility, a keyboard process (itself a synchronous activity) can asynchronously interrupt another process (presumably, the window selected at the point an interrupt character is seen).

(defvar *selected-window* nil)

(defun keyboard-process (raw-keyboard-stream)
  (loop (let ((char (read-char raw-keyboard-stream)))
          (when *selected-window*
            (if (is-interrupt-character? char)
                (keyboard-interrupt (window-process *selected-window*)
                                    char)
                ;; otherwise...
                (add-to-input-buffer *selected-window* char))))))

Although the KEYBOARD-PROCESS shown here will interrupt the window process, an understanding of what happens at that point does not require any special knowledge of asynchrony. It merely requires observing that at the time of interruption, the other process was about to execute some expression (exp) and will now execute instead

(progn (funcall interrupt-function) (exp))

This kind of structured approach removes much of the mystery and unpredictability normally associated with asynchronous interrupts in other systems, where the description of the effect is often not linguistic at all but deals in overly concrete terms of bits and registers in a way that only career experts can hope to navigate. The sense in the Common Lisp community is that a correct conceptual treatment of these issues makes these sorts of capabilities something that "mere mortals" can safely and conveniently employ in their programming.

Open Issues

The Dylan language patterned its efforts after the Common Lisp Condition System, but made some interesting changes. I probably lack the appropriate experience and surely the appropriate objectivity to conclude whether their changes are clear improvements over the Common Lisp approaches. But it's plain that by making divergent decisions in some places, the Dylan community has identified certain areas of the Common Lisp design as "controversial".

Restarts vs Handlers

Common Lisp provides parallel but unrelated operators such as HANDLER-BIND and HANDLER-CASE for dealing with handlers, and RESTART-BIND and RESTART-CASE for dealing with restarts. It was thought that these were orthogonal operations, requiring unrelated dataflow, that really didn't belong intermingled. The Dylan community has sought to coalesce these by making restarts into a kind of condition, and eliminating special binding forms for them.

The "Condition Firewall"

Probably the most controversial semantic component of the Common Lisp condition system is what has come to be called the "condition firewall". The idea behind the condition firewall is that a given handler should be executed in an environment that does not "see" intervening handlers that might have been established since its establishment.

So, for example, consider this code:

 (handler-case 
     (handler-bind ((error 
                      #'(lambda (condition)
                          (declare (ignore condition)) 
                          (error 'unbound-variable :name 'fred))))
        (handler-case ;; Signal an arbitrary error:
                      (error "Not an UNBOUND-VARIABLE error.")
          (unbound-variable (c) (list 'inner c))))
   (unbound-variable (c) (list 'outer c)))

This sets up two handlers for conditions of class UNBOUND-VARIABLE, one outside of the scope of the general-purpose handler for conditions of class ERROR and one inside of its scope. At the time the "arbitrary" error signaled, both handlers are in effect. This means that if the error being signaled had been of class UNBOUND-VARIABLE, it would have been caught by the inner HANDLER-CASE for UNBOUND-VARIABLE. However, as the search for a handler proceeds outward, the handlers that are tried are executed in a context where the inner handlers are no longer visible. As such, the above example yields

 (OUTER #<ERROR UNBOUND-VARIABLE 12A39B87>)

By contrast, the following code:

 (handler-case 
     (handler-bind ((error #'(lambda (condition)
                               (declare (ignore condition)) 
                               (error 'unbound-variable :name 'fred))))
       (handler-case (error 'unbound-variable :name 'marvin)
         (unbound-variable (c) (list 'inner c))))
   (unbound-variable (c) (list 'outer c)))

yields

 (INNER #<ERROR UNBOUND-VARIABLE 12A39B87>)

It is interesting to note as an aside that the "resignaling trick" used earlier in the discussion of default handling relies implicitly on the condition firewall in order to avoid infinite recursion. Without the condition firewall, a different mechanism for implementing default handling is needed.

The designers of the Dylan language chose to eliminate the condition firewall, perhaps out of necessity since the most useful restarts almost always occur in the dynamic space near the point of signal, and the handlers usually occur farther out. If handlers could only see the restarts farther out than where they were established, they would not see the most useful restarts. (I am personally doubtful of this argument, and am more inclined to believe that this is why restarts should not have been turned into a kind of condition in Dylan, but I could be wrong and time will tell.)

The Dylan notation is different in many ways from Common Lisp, but the approximately equivalent code to the above two examples would both, I believe, return

 (INNER #<ERROR UNBOUND-VARIABLE 12A39B87>)

Summary

Language features don't originate spontaneously out of nowhere. We have surveyed some of the origins of the Common Lisp Condition System in an effort to demonstrate how prior experiences, both good and bad, have influenced the present design. Nor is this the end of the story. The ideas in Common Lisp have had some influence on other languages and will, I hope, continue to do so, since there are a number of things the Common Lisp makes easy through its condition system that other languages do not.

We have also seen that good terminology is important both to the specification of a programming language and to its community acceptance. Programming is not only a technical endeavor, but a social one. So much of so many lives is spent doing programming, that it is critical that we have good terminology, beyond the terms of the language itself, for talking among each other about what we are doing within the language.

And we have surveyed some of the key features that distinguish Common Lisp's condition system from those offered by other languages, and highlighted some open issues, where Common Lisp's answers to certain problems have already met with challenges.

A Personal Footnote

During the design of Common Lisp, I headed the committee that produced the design of the condition system. At that time, there were many questions and doubts about the design: Were the decisions sound? Were all of the alternatives explored, or were there better ways we might later wish we'd tried? Were there problems lurking under the surface, waiting to bite someone when used under heavier stress?

It wasn't that people doubted our committee's competence, but rather many would-be reviewers lacked the relevant experience to critically analyze our proposals. Yet the design seemed mostly right to me, and my larger concern was that if we didn't at some point release it to a community of users to try, we'd be back at the same design table a few years later with the same questions and the same lack of community experience to answer them. A leap of faith seemed to be required to move ahead. So I and my committee nodded our collective heads and said we stood by the design. Personally, I had some doubts about some details, but I found it counterproductive to raise them because I believed the risk of not trying these things out was higher than the risk of trying them.

In my experience, much of language design is like this. We think we know how it will all come out, but we don't always. Usage patterns are often surprising, as one learns if one is around long enough to design a language or two and then watch how expectations play out in reality over a course of years. So it's a gamble. But the only way not to gamble is not to move ahead.

I once saw an interview on television with a font designer from Bitstream Inc. about how he conceptualized the process of font design. It is not about designing the shape of the letters, he explained, much to my initial surprise. Then he went on to explain that it was really about the shape of words. The font shapes play into that, but they are not, in themselves, the end goal. Programming language design is like that, too. It's not about the semantics of individual operators, but about how those operators fit together to form sentences in programs.

Unlike the situation with fonts, where whole books can be viewed instantly in a new font to see how the design works, we don't know in advance what sentences will be made in a programming language. We have to wait and see what people choose to write. Common Lisp took a step forward, and while we can quibble endlessly over whether any given design decision was right, the one design decision I'm most certain was right was to offer the community a rich set of capabilities that would empower them not only to write programs, but also to have a stake in future designs. Never again will I fear sending out e-mail to a design group asking for advice about what the semantics of HANDLER-BIND should be and finding that no one has an opinion! To me, that kind of progress, the evolution of a whole community's understanding, is the best kind of progress of all.

Acknowledgements

I would like to thank Keith Corbett, Christophe Dony, Bernard Greenberg, and Erik Naggum for reviewing drafts of this text. Any lingering errors after they got done looking at it are still my responsibility, but I'm quite sure the editorial, technical, and historical quality of this text was improved measurably through their helpful scrutiny.

Bibliography

[Multicians 2000]
Historical references to Multics can probably best be obtained from
http://www.multicians.org/
[Pitman 1983]
Kent M. Pitman, The Revised Maclisp Manual, Technical Report 295, MIT Laboratory for Computer Science, Cambridge, MA, May 1983.
[Pitman 1990]
Kent M. Pitman, "Exceptional Situations in Lisp", proceedings for the First European Conference on the Practical Application of Lisp (EUROPAL'90), Churchill College, Cambridge, UK, March 27-29, 1990.
http://www.nhplace.com/kent/Papers/Exceptional-Situations-1990.html
[Smith 1982]
B.C. Smith, Reflection and Semantics in a Procedural Language, Technical Report 272, MIT Laboratory for Computer Science, Cambridge, MA, January 1982.
[X3J13 1994]
American National Standard for Information Systems--Programming Language--Common Lisp (X3.226-1994)
http://www.lispworks.com/documentation/HyperSpec/Front/
[Weinreb 1983]
D.L. Weinreb and D.A. Moon, Lisp Machine Manual, MIT Artificial Intelligence Laboratory, Cambridge, MA, July 1981.

Original printed text document
Copyright 2001 by Kent M. Pitman. All Rights Reserved.

HTML hypertext version of document
Copyright 2002, Kent M. Pitman. All rights reserved.
The following limited, non-exclusive, revokable licenses are granted:

Browsing of this document (that is, transmission and display of a temporary copy of this document for the ordinary purpose of direct viewing by a human being in the usual manner that hypertext browsers permit such viewing) is expressly permitted, provided that no recopying, redistribution, redisplay, or retransmission is made of any such copy.

Bookmarking of this document (that is, recording only the document's title and Uniform Resource Locator, or URL, but not its content, for the purpose of remembering an association between the document's title and the URL, and/or for the purpose of making a subsequent request for a fresh copy of the content named by that URL) is also expressly permitted.

All other uses require negotiated permission.


Click here for an index of other titles by Kent Pitman.