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!