Zachary W. Huang
May 7, 2026
I think Common Lisp is a very interesting language for a variety of reasons, and today I’ll go over one language feature it has that not many other languages have. That feature is the condition system.
Common Lisp is very dynamic, not just with types, but also with control flow. For example, you can return from anywhere in a block of code, which you might not expect from a “functional” language.
(block early-exit
(format t "Hello, world!~%")
(return-from early-exit 100)
(format t "Division by zero! ~D~%" (/ 1 0)))
; Hello, world!
; => 100
As you can see, we can define a block with a label, and then jump to it directly using return-from, without evaluating the rest of the forms in the block.
Note that this type of control transfer is lexical.
So we can, for example, define a function that returns the first value in a list that satisfies some predicate in a very simple manner.
(defun find-first (list predicate)
"Returns the first element in a list that satisfies a predicate, otherwise NIL"
; loop over elements in list
(dolist (l list)
(if (funcall predicate l)
;; dolist automatically defines "block nil"
(return l))))
(find-first (list 1 2 3) #'evenp) ; => 2
If a block tag is not in the lexical scope of a return-from, you’ll get a compile error.
It’s no different from trying to use a variable that hasn’t been defined.
(defun bar-1 (x)
(block early-exit
; Only establishes early-exit in this lexical scope
(bar-2 x)
(format t "Division by zero! ~D~%" (/ 1 0))))
(defun bar-2 (x)
; Doesn't "know" about the early-exit block
(return-from early-exit x))
; Compile error:
; in: DEFUN BAR-2
; (RETURN-FROM EARLY-EXIT X)
;
; caught ERROR:
; return for unknown block: EARLY-EXIT
Common Lisp goes even further and defines a special operator called tagbody which provides C-like “goto” semantics using the go special operator.
You can write a loop like this:
(defun count-to (y)
(let ((x 1))
(tagbody
start
(when (> x y)
(go end))
loop
(format t "x = ~D~%" x)
(when (< x y)
(incf x)
(go loop))
end)))
; * (count-to 5)
; x = 1
; x = 2
; x = 3
; x = 4
; x = 5
Note again that this control flow is still “lexical” - the target of go must exist somewhere in the tagbody, otherwise the code will fail to compile.
Common Lisp also provides dynamic transfers of control using catch and throw.
These are most similar to exceptions in other programming languages.
When you use the catch operator, it adds itself as a “handler” in the current runtime environment before executing its body.
Then, the throw operator occurs later on, it will unwind the stack until it finds a matching catch, and then automatically exit the block with the thrown value.
So we can rewrite our previous example with block and return-from with catch and throw, which would look something like:
(defun bar-1 (x)
(catch 'early-exit
(bar-2 x)
(format t "Division by zero! ~D~%" (/ 1 0))))
(defun bar-2 (x)
(throw 'early-exit x))
; * (bar-1 42)
; => 42
Because the 'early-exit tag was established before the code in bar-2 ran, the throw is able to find the right catch form and immediately return from it, avoiding the division by zero error.
Notice that this is an example of dynamic extent, which is essentially defined by Common Lisp as a scope that is bounded by some “establishment” and “disestablishment”.
In this case, the catch form “establishes” the scope for the 'early-exit tag, and so any code that is called within the body of the catch is able to refer to 'early-exit.
Once you exit the body of the catch, then the tag is then “disestablished”, and you can’t throw to it anymore.
For example, if you just call (bar-2 42) on its own, you would get an error saying the 'early-exit tag doesn’t exist.
These forms are implemented similarly to exception handlers in more traditional programming languages like Python and Java.
When the code hits a throw, it starts to unwind the call stack until it finds a matching handler.
And if no handler is defined, then the program errors out.
In some cases, you might want to ensure some code runs even if the stack is being unwound due to a throw.
Similarly to the finally keywords in Python and Java, Common Lisp provides the unwind-protect operator for this purpose.
For example, you might want to ensure that a resource is cleaned up even if an error occurs, like so:
;; Function that throws an error
(defun oopsie (stream)
(format t "Oops, an error occured!~%")
(throw 'oh-no! "Error message"))
(defun write-to (filename)
;; Open a file stream
(let ((stream (open filename :direction :output :if-exists :supersede)))
;; Jump here if error
(catch 'oh-no!
;; But before that, remember to run cleanup code
(unwind-protect
;; Code that might error
(progn ; execute these in series
(oopsie stream)
(format stream "Hello from the file!~%"))
;; Cleanup code
(format t "Cleanup: Closing the file stream now.~%")
(close stream)))))
; * (write-to "hello.txt")
; Oops, an error occurred!
; Cleanup: Closing the file stream now.
; => "Error Message"
We see that in the write-to function, we open a file and try to write to it.
However, the oopsie function throws an error.
If we didn’t have the unwind-protect, then the program would forget to close the file stream, potentially resulting in a memory/resource leak.
This is basically what the with-open-file macro in the standard library does.
While catch and throw are more familiar, they are still quite primitive, and Common Lisp provides an even more powerful feature that essentially generalizes the idea of an exception.
Conditions provide a more object-oriented approach to handling exceptions.
However, they can also be used for more than just traditional exception handling, which I’ll get to later.
In Common Lisp, a condition is essentially meant to model an “exceptional situation”, which may or may not be an actual error.
Since a condition is just a regular class in Common Lisp, you can define your own conditions that inherit from it and can be easily integrated into your own code.
; Define our own condition type with a custom message
(define-condition hello (condition)
((what :initarg :what :reader hello-what))
(:report (lambda (this stream)
(format stream "Hello, ~A!"
(hello-what this)))))
Note that when a condition is signalled, the stack is not automatically unwound, unlike catch and throw.
This means that it is possible (or even desired) for code to continue execution after signalling a condition, and we’ll go into this more soon.
Instead, when a condition is signalled, it will look at all of the potential handler functions that were previously established using handler-bind and pick the innermost one to call.
But first, what exactly is a handler function?
A handler function is just a regular function that takes in a condition as its argument. However, a handler function must do one of three things: 1) it can decline to handle the condition by returning, 2) it can “handle” the condition by performing a nonlocal transfer of control, or 3) it can defer to another handler by re-signaling the condition or signaling a different condition.
So if a handler declines (the first case), then it will look for the next most recently established handler function and repeat.
If we perform a nonlocal jump, then the condition is considered “handled”, potentially unwinding the stack.
Otherwise, another condition may be signalled, and this repeats.
Our mental model can be that handler-bind keeps adding handlers to an internal stack of condition handlers, which a condition tries in series until it runs out, or is successfully “handled”.
For example, consider the handler function below.
(defun handle-hello (this)
(format t "Declining to handle Hello with ~A~%" (hello-what this)))
(defmacro with-hello (&body body)
`(handler-bind ((hello #'handle-hello))
,@body))
There are also multiple ways in which we can signal a condition.
If we use the signal function, Common Lisp will execute the corresponding handler, and then continue like normal (unless control is transferred).
Even if the condition wasn’t successfully handled, the signal will simply return nil, and execution continues.
(with-hello
(format t "Before signal~%")
(signal 'hello :what 1234)
(format t "After signal~%"))
; This code will print:
; Before signal
; Handling a hello condition with 1234
; After signal
If it is imperative that a condition be handled (like for an exception of some kind), then we can use the error function.
Unlike signal, if the condition is not handled, then Common Lisp will enter the interactive debugger, and execution cannot continue.
(with-hello
(format t "Before error~%")
(error 'hello :what 1234)
(format t "After error~%"))
; This code will print:
; debugger invoked on a HELLO in thread
; #<THREAD tid=259 "main thread" RUNNING {7004E10003}>:
; Hello, 1234!
;
; Type HELP for debugger help, or (SB-EXT:EXIT) to exit from SBCL.
;
; 0]
Notice that we get the nice custom error message we specified using the :report keyword when we defined the condition.
By combining condition handlers with catch and throw from before, we can get a slightly more advanced error handling mechanism.
We can label the “exit” point using a catch, and then throw to that exit point inside if our condition handler.
(defun handle-hello-end (this)
(format t "Handling hello~%")
(throw 'hello-end nil))
(defmacro catch-hello (&body body)
`(catch 'hello-end
(handler-bind ((hello #'handle-hello-end))
,@body)))
(catch-hello
(format t "Before error~%")
(error 'hello :what 1234)
(format t "After error~%"))
; This code will print:
; Before error
; Handling hello
So now, our catch-hello macro establishes a condition handler that simply handles an error condition by breaking out of the catch form, preventing you from entering the interactive debugger like before.
This is all well and good, but now notice that we can do kind of a funny thing - by moving around the catch operator, we can decide exactly where to continue execution, which could very well just be… the next line of code!
(defun handle-hello-continue (this)
(format t "Handling hello~%")
(throw 'continue nil))
(defmacro with-hello-ignore (&body body)
`(handler-bind ((hello #'handle-hello-continue))
,@body))
(with-hello-ignore
(format t "Before error~%")
(catch 'continue
(error 'hello :what 1234))
(format t "After error~%"))
; This code will print:
; Before error
; Handling hello
; After error
So even though we’re signalling an error, Common Lisp allows us to just ignore the error and keep going, so long as we’re using the right handler function. This might not seem all that interesting, but it gets to a key part of Common Lisp’s dynamicism: the fact that you can alter control flow inside of a block of code by choosing which handler you install on the outside.
So now we’ve come across the last idea in Common Lisp’s condition system: restarts.
Instead of using primitive catch and throw, we can now specify how to “recover” from a condition, using restart-bind and invoke-restart.
However, there is a key difference: restarts run code in the same dynamic environment they were invoked from.
And this is exactly because signalling a conditions doesn’t unwind the stack unless there is a non-local transfer (or error).
So restarts can actually modify the dynamic variables that exist inside of a block of code, from the outside.
Or, for a less invasive approach, restarts can also be used to simply pass messages from the outside into the system.
We can rewrite our “continue” handler from before like so:
(defun handle-hello-continue (this)
(format t "Handling hello~%")
(invoke-restart 'continue))
(defmacro with-hello-continue (&body body)
`(handler-bind ((hello #'handle-hello-continue))
,@body))
(with-hello-continue
(tagbody
(format t "Before error~%")
(restart-bind ((continue #'(lambda () (go next))))
(error 'hello :what 1234))
next
(format t "After error~%")))
; This code will print:
; Before error
; Handling hello
; After error
To be fair, this may seem less clean than the catch/throw approach.
But the restart system is flexible since now you can define arbitrary restart behaviors, with messages being sent in/out of the system through conditions and restarts.
For example, one commonly used restart is 'use-value, which simply passes a value from an outside handler into a restart.
This gives you something close to algebraic effects (but without first-class continuations).
For example, here is an example of injecting a value (like a password) into some code using a restart.
; Dummy condition (just used for its name)
(define-condition get-password (condition) ())
(defun login ()
(let ((password nil))
; If received restart named "use-password", set that value
(restart-bind ((use-password #'(lambda (v) (setf password v))))
; Request the "get-password" handler
(signal 'get-password))
; Check what our password is (if none received, then NIL)
(format t "Using password: ~A~%" password)))
(defun password-handler (c)
; Handle a get-password condition by restarting with the password
(when (find-restart 'use-password)
(invoke-restart 'use-password "SECRET")))
(defun main ()
; Try logging in without the "get-password" handler
; Prints out "Using password: NIL"
(login)
; Now try logging in with the handler
; Prints out "Using password: SECRET"
(handler-bind ((get-password #'password-handler))
(login)))
This is definitely a more basic example, and arguably it has quite a bit of boilerplate, but since you can execute arbitrary code and logic within both conditions and restarts, you get a ton of flexibility in your program structure, error handling, and control flow. You can do whatever you want!
Since handler-bind and restart-bind can be slightly verbose, Common Lisp also defines handler-case and restart-case to cover the most common sorts of handler/restart use cases.
You can think of these -case forms as basically match statements, but they match on which handler or restart was invoked.
For example, consider the following code:
(handler-case
(progn
; Comment out each of these lines to see what happens
(signal 'simple-condition)
(error 'serious-condition)
0
)
;; Match against these handlers, then jump to their body if matches
(simple-condition (c)
(format t "Simple condition, no problem!~%")
1234)
(serious-condition (c)
(format t "Serious condition, uh oh!~%")
4321)
; Jump here if no conditions
(:no-error (v)
(format t "Phew, no conditions.~%")
v))
This gives you something quite similar to conventional error handling, since the handler-case automatically performs a transfer of control from where the condition was raised into the body of the handler for each type of condition.
The macro roughly expands its contents in terms of handler-bind as follows:
(handler-case
<body>
(condition-1 (<var1>) <condition-1-body>)
(condition-2 (<var2>) <condition-2-body>)
...
(:no-error (<var-no-error>) <no-error-body>))
; =>
(block error-return
(block no-error-return
(let ((ret nil))
(tagbody
(handler-bind ((condition-1 #'(lambda (temp)
(setf ret temp)
(go after-condition-1)))
(condition-2 #'(lambda (temp)
(setf ret temp)
(go after-condition-2)))
...)
(setf ret <body>)
(go no-error))
no-error
(return-from no-error-return
(let ((<var-no-error> ret)) <no-error-body>))
after-condition-1
(return-from error-return
(let ((<var1> ret)) <condition-1-body>))
after-condition-2
(return-from error-return
(let ((<var2> ret)) <condition-2-body>))))))
The restart-case macro is similar, but it matches on restarts instead of conditions.
So we can write something like:
(define-condition bad-data (error)
((item :initarg :item :reader bad-item)))
(defun process-item (item)
"Multiplies a number by 10. Offers restarts if the item isn't a number."
(restart-case
(if (numberp item)
(* item 10)
(error 'bad-data :item item))
; Restart Option 1: Provide a fallback value
(use-value (new-value)
(* new-value 10))
; Restart Option 2: Just skip it
(skip-item ()
nil)))
(defun process-all-data (data-list)
"Loops through the data. Notice there is no error handling here."
(loop for item in data-list
for result = (process-item item)
when result collect result))
(defun process-with-replacement (data)
(handler-bind ((bad-data (lambda (condition)
; restart with default value
(invoke-restart 'use-value 0))))
(process-all-data data)))
(defun process-with-skipping (data)
(handler-bind ((bad-data (lambda (condition)
; restart by skipping
(invoke-restart 'skip-item))))
(process-all-data data)))
(let ((data '(1 2 "corrupted" 4 5 "broken" 7)))
(format t "With replacement: ~A~%" (process-with-replacement data))
(format t "With skipping: ~A~%" (process-with-skipping data)))
; With replacement: (10 20 0 40 50 0 70)
; With skipping: (10 20 40 50 70)
The restart-case macro expands out very similarly to handler-case, so I won’t write it here again.
While these two macros are named similarly, they are used quite differently: handler-case automatically unwinds the stack, so it’s more like a nicer-looking catch/throw, while restart-case performs a restart in the context of the original code, so it’s used to “hide” multiple types of behaviors behind one form (the process-item in the example above) and is typically used in conjunction with handler-bind.
In essence, using handlers and restarts together is supposed to help with program organization. They are kept as orthogonal language features to maximize flexibility - the handler decides what kind of “policy” to implement, and the restart performs the actual “recovery” mechanism. So you get a sequence that looks like “signal condition” -> “decide how to handle condition” -> “actually ‘handle’ the condition”, in which you can have arbitrary logic at any of these steps.
So, we’ve made it pretty far.
There are still a few things I omitted, like the default conditions and restarts provided by the language, but I don’t think those are as important as the concepts themselves.
There are also two more ways to signal conditions (cerror and warn), some helpful utilities like ignore-errors, assert, interactive restarts, custom error messages, etc etc.
If you’re interested, you can look into them yourself!
Some good resources for the condition system are:
And most of these contain much more helpful information about Common Lisp in general. I hope this blog post piques your interest in Common Lisp!