Previous section: Identity function

Dylan reference manual -- Background

13. Conditions

Background

A long-standing problem of software engineering is the need to develop an organized way to deal with exceptions, situations that must be handled gracefully but that are not conceptually part of the normal operation of the program. A number of programming languages contain linguistic features for exceptions, among them Common Lisp, C++, PL/I, and Ada.

Of course it is possible to program exception handling without using special linguistic features. For example, all functions could return an extra result that indicates whether they succeeded or failed, functions could take an extra argument that they consult if an exception occurs, or a designated exception-handling function could be called whenever a problem arises. All of these approaches have been used in one real-life system or another, but they are deficient in two ways. First, they are too informal and don't provide enough structure to allow an organized, systematic approach to exception handling. Second, and more importantly, the first two approaches do not provide textual separation between "normal code" and "code for dealing with exceptions"; exception-related code is sprinkled throughout the program. This leads to two problems: one is the well-known mistake of forgetting to test error codes and thus failing to detect an exception, perhaps one that "could never happen;" the other is that program clarity is lost because it isn't easy to think about the main flow of the program while temporarily ignoring exceptions.

Thus, the most important requirements of a linguistic exception-handling facility are to provide overall structure, to eliminate the possibility of failing to notice an exception, and to provide a clean separation between "normal code" and "code for dealing with exceptions."

All exception systems involve the concept of "signal" (sometimes with a different name, such as "raise" or "throw") and the concept of "handle" (sometimes with a different name such as "on-unit" or "catch"). All exception systems considered here dynamically match signalers with handlers, first invoking the most recently established matching handler still active, and then, if that matching handler declines to handle the exception, invoking the next most recent matching handler, and so on. Thus, exception systems really are a way of establishing a run-time connection between a signaler and a handler, as distinct from the usual fixed connection between a caller and a callee through function-name matching.

The major design choices in exception systems--name-based versus object-based, calling versus terminating, and formal versus ad-hoc recovery--are discussed in the following paragraphs.

PL/I and Ada have name-based exception systems. A program signals a name, and a handler matches if it handles the same name or "any." The name is a constant in the source text of the program, not the result of an expression.

C++ and Common Lisp have object-based exception systems. A program signals an object, and a handler matches if it handles a type that object belongs to. Object-based exceptions are more powerful, because the object can communicate additional information from the signaler to the handler, because the object to be signaled can be chosen at run-time rather than signaling a fixed name, and because type inheritance in the handler matching adds abstraction and provides an organizing framework.

The C++ object-based exception system has complicated inheritance rules and allows any object whatsoever to be used as an exception. Common Lisp, in contrast, allows only objects that inherit from a certain built-in class to be used as exceptions, thus limiting the inheritance rules to class inheritance and providing a class on which to hang any protocols specific to exceptions.

Ada and C++ have terminating exception systems: before a handler receives control, all dynamic state between the handler and the signaler is unwound, as if signaling were a non-local goto from the signaler to the handler. PL/I has a calling exception system: when a handler receives control, the signaler is still active and control can be returned to it, as if signaling were a function call from the signaler to the handler. Common Lisp has both, with the calling semantics regarded as fundamental and the terminating semantics explained in terms of it. Note that calling semantics need not imply that the signal operation can return the way a function call returns; Common Lisp provides a "restart" mechanism that signalers use to grant handlers permission to return. The Common Lisp model shows that terminating versus calling is a property of handlers, not of exception systems as a whole.

Terminating exception systems are acceptable for errors. However, they do not work for an exception that is not an error and doesn't require termination, either because there is a default way to handle it and recover or because it can safely be ignored by applications that don't care about it. Non-error exceptions are quite common in networked environments, in computers with gradually expiring resources (such as batteries), in complex user interfaces, and as one approach for reflecting hardware exceptions such as page protection violations or floating-point overflow to the application.

Most languages have not formalized how to recover from exceptions, leaving programmers to invent ad hoc mechanisms. Common Lisp provides "restarts," which is an object-based way of establishing a run-time connection from a handler back to the signaler or to any other part of the dynamically active program. A handler can choose to return control to any available restart, or else can mimic terminating semantics, using the language's normal non-local exit mechanisms. Restarts can also be a good way for programs to indicate to an interactive debugger what the end user's choices are for recovery from an exception that lands in the debugger. In this sense, the debugger can be understood as just a complicated handler. Unfortunately some details of restarts are somewhat poorly designed in Common Lisp.

It is necessary to have a way to clean up when execution of a function is terminated by a non-local exit initiated either by the function itself or by something it explicitly or implicitly called. In C++ this includes invoking destructors for automatic objects, as well as application-specific cleanup. Ada, C++, and the Multics dialect of PL/I associate this action with exception handling. Common Lisp realizes it is related but different and calls it unwind-protect. Dylan uses the Common Lisp approach, except that Common Lisp's unwind-protect becomes a cleanup clause in Dylan's block statement.

For detailed information on exceptions in Common Lisp, refer to chapter 29 of Common Lisp: the Language, Second Edition, by Guy L. Steele Jr. For detailed information on exceptions in C++ (not yet implemented in many C++ implementations but adopted into the draft proposed ANSI C++ standard), refer to chapter 15 of The Annotated C++ Reference Manual, by Margaret A. Ellis and Bjarne Stroustrup. Although it is not necessary to read either document to understand the Dylan condition system, they may be helpful as sources of background information.

Next section: Overview