A Short Dive into the Common Lisp Condition System
Table of Contents
The Flow of Exception Handling
Concepts
Debugging
Common Lisp is notoriously known for employing some of the most advanced programming concepts albeit being a decades old language. A cornerstone of the unique, repl-driven / interactive or image-driven development workflow is its condition system that allows recovering from unforseen events (arguably) in an elegant fashion and provides many of the facilities that allow updating, enhancing and fixing applications during runtime.
I find it a bit confusing, so I try to show a few concepts below. The different paths show a set of possible configurations a program may carry on the occurence of a condition. In this overview I skip the whole concept of defining custom conditions. We start from the top where a condition is either signalled or errored and end with one of the 5 different results displayed at the bottom.

The Flow of Exception Handling
- Condition Raised: This is the starting point where an exceptional situation is detected, and a condition object is created to represent it. It may carry information on program state and matching on custom types of conditions is a common concept.
Signal vs. Error: The condition can be raised using either the signal or error function.- signal: Signals a condition without initiating stack unwinding. Control may return to the point immediately after the signal call if the condition is not handled.
- error: Signals a serious condition and initiates stack unwinding if the condition is not handled.
- handler-bind Present?: The system checks if there is a dynamically established handler for the condition using handler-bind.
- Handler Found: If a handler is found, it is executed.
- Altering Control Flow: If the handler alters the control flow (e.g., by invoking a restart or transferring control), the program flow changes accordingly and we enter some part of the program unrelated to the original program flow. This case is therefore independent of the handling occuring due to error or signal and in consequence, our primary mechanism of preserving control flow (and preventing stack unwinding) on error.
- Not Altering Control Flow: If the handler does not alter the flow, the condition is propagated further depending on whether a signal or error occured.
- No Handler Found: If no handler is found, the system determines whether the condition was signaled or errored: The former is ignored with regular program flow continuing, the latter enforces unwinding of the stack and enters the handler-case structure if present.
- Signaled Condition: If the condition was signaled and not handled, execution continues from the point immediately after the signal call with regular program flow.
- Errored Condition: If the condition was errored and not handled, stack unwinding is enforced resulting in handler-case handling, interactive debugging or program termination.
- Handler Found: If a handler is found, it is executed.
- Stack Unwinding and handler-case
- Stack Unwinding: The process of unwinding the call stack to find an appropriate handler or to clean up before termination.
- handler-case Present?: During stack unwinding, the system checks for any handler-case forms.
- Handler Found: If a handler-case is found, its handler is executed, and execution continues from the handler-case form.
- No Handler Found: If no handler-case is found, the system checks if it’s in an interactive environment. If interactive, the debugger is invoked, allowing the programmer to inspect and possibly recover from the error, modify values, overwrite functions, pretty much execute arbitrary program modifications and rerun/continue. Otherwise the program terminates.
- signal Function:
- Used for less severe conditions that the program might recover from.
- Does not initiate stack unwinding if unhandled.
- If no handler is found, execution continues as if nothing happened.
- error Function:
- Used for serious conditions that typically require termination or significant recovery actions.
- Initiates stack unwinding if unhandled.
- If no handler is found, may lead to program termination or invocation of the debugger.
- handler-bind:
- Establishes dynamic handlers for conditions.
- Handlers can intercept conditions before stack unwinding occurs.
- Useful for temporarily handling conditions during the execution of a specific block of code.
- Does not inherently alter the control flow unless the handler explicitly does so.
- handler-case:
- Similar to a try-catch block in other languages.
- Handles conditions that occur during stack unwinding.
- Allows for recovery or cleanup actions before resuming execution or terminating.
- The control flow continues from the point after the handler-case form if a handler handles the condition.
Invoking RestartsRestarts in Common Lisp are predefined or user-defined mechanisms that provide options for recovering from errors. They act as labeled points in the program that can be invoked to alter the control flow during exception handling.
- Invoking a Restart: Within a handler or the debugger, you can invoke a restart using the invoke-restart function.
- Effect on Control Flow: Invoking a restart transfers control to the point where the restart was established, potentially with arguments to influence behavior.
You can define your own restarts using the restart-case macro or by establishing restarts with with-simple-restart or restart-bind. Custom restarts allow you to offer specific recovery strategies that can be interactively chosen when an error occurs.
- Interactive Selection: When in the debugger, the available restarts are presented as options, and you can choose one to recover from the error.
- Providing Recovery Options: Custom restarts can perform actions like retrying an operation, providing default values, or skipping over problematic code.
(defun divide (a b)
"Divides A by B, signaling an error if B is zero."
(if (zerop b)
(error 'division-by-zero :numerator a :denominator b)
(/ a b)))
(defun safe-divide (a b)
"Performs division with custom restarts for error recovery."
(restart-case
(divide a b)
(use-default ()
:report "Use 1 as the default denominator."
(divide a 1))
(specify-new-denominator (new-denominator)
:report "Specify a new denominator."
:interactive (lambda () (list (parse-integer (read-line))))
(divide a new-denominator))
(return-nil ()
:report "Return NIL instead of performing division."
nil)))
;; Example usage
(let ((result (safe-divide 10 0)))
(format t "Result: ~a~%" result))
debugger invoked on a DIVISION-BY-ZERO in thread
#<THREAD tid=77820 "main thread" RUNNING {10012D8003}>:
arithmetic error DIVISION-BY-ZERO signalled
Type HELP for debugger help, or (SB-EXT:EXIT) to exit from SBCL.
restarts (invokable by number or by possibly-abbreviated name):
0: [USE-DEFAULT ] Use 1 as the default denominator.
1: [SPECIFY-NEW-DENOMINATOR] Specify a new denominator.
2: [RETURN-NIL ] Return NIL instead of performing division.
3: [RETRY ] Retry EVAL of current toplevel form.
4: [CONTINUE ] Ignore error and continue loading file "/tmp/dud.lisp".
5: [ABORT ] Abort loading file "/tmp/dud.lisp".
6: Ignore runtime option --load "/tmp/dud.lisp".
7: Skip rest of --eval and --load options.
8: Skip to toplevel READ/EVAL/PRINT loop.
9: [EXIT ] Exit SBCL (calling #'EXIT, killing the process).
(defun auto-fix-divide (a b)
"Attempts to divide, automatically using default restart on error."
(handler-bind ((division-by-zero
(lambda (c)
(invoke-restart 'use-default))))
(safe-divide a b)))
All this is pretty powerful but also quite complicated. For me the distinction between signaling and erroring is especially puzzling because I learned exception handling from the family of C-like languages where the concept of signaling is not really covered.