I updated the Condition Handling page on the Common Lisp Cookbook to show what it means that handler-bind
“doesn’t unwind the stack”, along with a couple real-world use cases.
This time I must thank Ari. I originally wrote the page (with
contributions from jsjolen, rprimus and phoe), starting from Timmy
Jose “z0ltan”’s article (linked in the introduction) and the books I
had at my disposal. These don’t show handler-bind
’s power and
use-cases like this, focusing on restarts (which this page certainly
could explain better, it shows they aren’t part of my daily
toolbelt). So I learned about the real goodness by chance while
reading @shinmera’s code and, shame on me, I didn’t update the
Cookbook. My video course has been well and complete for years though
;) I needed fresh eyes and that happened with Ari. He asked if I
offered 1-1 video lisp mentoring. I didn’t, but now I do!. So, as
we talked about a million things (lisp and software development in
general) and eventually worked on condition handling, I realized this
page was lacking, and I took an extra 1h 40min to update it,
“backporting” content from my video course.
These days my attention is more turned towards my tutorials for web development in Common Lisp, my CL projects and libraries, and less to the Cookbook alone like before, but for this edit to the Cookbook 2 things were new and important:
- I had fresh eyes again, thanks to the lisp mentoring
- I had an extra motivation to do the edit in the context of the 1-1 session: I felt it’s part of the job. By the way, we settled on 40USD an hour and I have a couple more slots available in the week ;) Please contact me by email (@ vindarel (. mailz org)) Thanks! [/end of self plug].
So what is this all about? handler-bind
, unlike handler-case
,
doesn’t unwind the stack: it shows us the full backtrace and gives us
absolute control over conditions and restarts.
It’s particularly necessary if you want to print a meaningful
backtrace. We’ll give another development tip, where you can decide to either
print a backtrace (production mode) or accept to be dropped into the
debugger (invoke-debugger
).
Feel free to leave feedback, in the comments or in the Cookbook issues or PR.
Table of Contents
Absolute control over conditions and restarts: handler-bind
handler-bind is what to use when we need absolute control over what happens when a condition is signaled. It doesn’t unwind the stack, which we illustrate in the next section. It allows us to use the debugger and restarts, either interactively or programmatically.
Its general form is:
(handler-bind ((a-condition #'function-to-handle-it)
(another-one #'another-function))
(code that can...)
(...error out…)
(... with an implicit PROGN))
For example:
(defun handler-bind-example ()
(handler-bind
((error (lambda (c)
(format t "we handle this condition: ~a" c)
;; Try without this return-from: the error bubbles up
;; up to the interactive debugger.
(return-from handler-bind-example))))
(format t "starting example…~&")
(error "oh no")))
You’ll notice that its syntax is “in reverse” compared to
handler-case
: we have the bindings first, the forms (in an implicit
progn) next.
If the handler returns normally (it declines to handle the condition), the condition continues to bubble up, searching for another handler, and it will find the interactive debugger.
This is another difference from handler-case
: if our handler
function didn’t explicitely return from its calling function with
return-from handler-bind-example
, the error would continue to bubble
up, and we would get the interactive debugger.
This behaviour is particularly useful when your program signaled a simple condition. A simple condition isn’t an error (see our “conditions hierarchy” below) so it won’t trigger the debugger. You can do something to handle the condition (it’s a signal for something occuring in your application), and let the program continue.
If some library doesn’t handle all conditions and lets some bubble out
to us, we can see the restarts (established by restart-case
)
anywhere deep in the stack, including restarts established by other
libraries that this library called.
handler-bind doesn’t unwind the stack
With handler-bind
, we can see the full stack trace, with every
frame that was called. Once we use handler-case
, we “forget” many
steps of our program’s execution until the condition is handled: the
call stack is unwound (or “untangled”, “shortened”). handler-bind
does not rewind the
stack. Let’s illustrate this.
For the sake of our demonstration, we will use the library
trivial-backtrace
, which you can install with Quicklisp:
(ql:quickload "trivial-backtrace")
It is a wrapper around the implementations’ primitives such as sb-debug:print-backtrace
.
Consider the following code: our main
function calls a chain of
functions which ultimately fail by signaling an error
. We handle the
error in the main function with hander-case
and print the backtrace.
(defun f0 ()
(error "oh no"))
(defun f1 ()
(f0))
(defun f2 ()
(f1))
(defun main ()
(handler-case (f2)
(error (c)
(format t "in main, we handle: ~a" c)
(trivial-backtrace:print-backtrace c))))
This is the backtrace (only the first frames):
CL-REPL> (main)
in main, we handle: oh no
Date/time: 2025-07-04-11:25!
An unhandled error condition has been signalled: oh no
Backtrace for: #<SB-THREAD:THREAD "repl-thread" RUNNING {1008695453}>
0: […]
1: (TRIVIAL-BACKTRACE:PRINT-BACKTRACE … )
2: (MAIN)
[…]
So far so good. It is trivial-backtrace
that prints the “Date/time” and the message “An unhandled error condition…”.
Now compare the stacktrace when we use handler-bind
:
(defun main-no-stack-unwinding ()
(handler-bind
((error (lambda (c)
(format t "in main, we handle: ~a" c)
(trivial-backtrace:print-backtrace c)
(return-from main-no-stack-unwinding))))
(f2)))
CL-REPL> (main-no-stack-unwinding)
in main, we handle: oh no
Date/time: 2025-07-04-11:32!
An unhandled error condition has been signalled: oh no
Backtrace for: #<SB-THREAD:THREAD "repl-thread" RUNNING {1008695453}>
0: …
1: (TRIVIAL-BACKTRACE:PRINT-BACKTRACE …)
2: …
3: …
4: (ERROR "oh no")
5: (F0)
6: (F1)
7: (MAIN-NO-STACK-UNWINDING)
That’s right: you can see all the call stack: from the main function
to the error through f1
and f0
. These two intermediate functions
were not present in the backtrace when we used handler-case
because,
as the error was signaled and bubbled up in the call stack, the stack
was unwound, and we lost information.
When to use which?
handler-case
is enough when you expect a situation to fail. For
example, in the context of an HTTP request, it is a common to anticipate a 400-ish error:
;; using the dexador library.
(handler-case (dex:get "http://bad-url.lisp")
(dex:http-request-failed (e)
;; For 4xx or 5xx HTTP errors: it's OK, this can happen.
(format *error-output* "The server returned ~D" (dex:response-status e))))
In other exceptional situations, we’ll surely want handler-bind
. For
example, when we want to handle what went wrong and we want to print a
backtrace, or if we want to invoke the debugger manually (see below)
and see exactly what happened.
Invoking the debugger manually
Suppose you handle a condition with handler-bind
, and your condition
object is bound to the c
variable (as in our examples
above). Suppose a parameter of yours, say *devel-mode*
, tells you
are not in production. It may be handy to fire the debugger on the
given condition. Use:
(invoke-debugger c)
In production, you can print the backtrace instead and have an error reporting tool like Sentry notify you.
Closing words
This is yet another CL feature I wish I had known earlier and learned by chance. I hope you learned a thing or two!