A curious case of HANDLER-CASE

Tagged as lisp, hacks

Written on 2021-10-19 by Daniel 'jackdaniel' KochmaƄski

Common Lisp is known among Common Lisp programmers for its excellent condition system. There are two operators for handling conditions: handler-case and handler-bind:

(handler-case (do-something)
  (error (condition)
    (format *debug-io* "The error ~s has happened!" condition)))

(handler-bind ((error
                 (lambda (condition)
                   (format *debug-io* "The error ~s has happened!" condition))))
  (do-something))

Their syntax is different as well as semantics. The most important semantic difference is that handler-bind doesn't unwind the dynamic state (i.e the stack) and doesn't return on its own. On the other hand handler-case first unwinds the dynamic state, then executes the handler and finally returns.

What does it mean? When do-something signals an error, then:

  • handler-case prints "The error ... has happened!" and returns nil
  • handler-bind prints "The error ... has happened!" and does nothing

By "doing nothing" I mean that it does not handle the condition and the control flow invokes the next visible handler (i.e invokes a debugger). To prevent that it is enough to return from a block:

(block escape
  (handler-bind ((error
                   (lambda (condition)
                     (format *debug-io* "The error ~s has happened!" condition)
                     (return-from escape))))
    (do-something)))

With this it looks at a glance that both handler-case and handler-bind behave in a similar manner. That brings us to the essential part of this post: handler-case is not suitable for printing the backtrace! Try the following:

(defun do-something ()
  (error "Hello world!"))

(defun try-handler-case ()
  (handler-case (do-something)
    (error (condition)
      (trivial-backtrace:print-backtrace condition))))

(defun try-handler-bind ()
  (handler-bind ((error
                   (lambda (condition)
                     (trivial-backtrace:print-backtrace condition)
                     (return-from try-handler-bind))))
    (do-something)))

When we invoke try-handler-case then the top of the backtrace is

1: (TRIVIAL-BACKTRACE:PRINT-BACKTRACE #<SIMPLE-ERROR "Hello world!" {1002D77DD3}> :OUTPUT NIL :IF-EXISTS :APPEND :VERBOSE NIL)
2: ((FLET "FUN1" :IN TRY-HANDLER-CASE) #<SIMPLE-ERROR "Hello world!" {1002D77DD3}>)
3: (TRY-HANDLER-CASE)
4: (SB-INT:SIMPLE-EVAL-IN-LEXENV (TRY-HANDLER-CASE) #<NULL-LEXENV>)
5: (EVAL (TRY-HANDLER-CASE))

While when we invoke try-handler-bind then the backtrace contains the function do-something:

0: (TRIVIAL-BACKTRACE:PRINT-BACKTRACE-TO-STREAM #<SYNONYM-STREAM :SYMBOL SWANK::*CURRENT-DEBUG-IO* {1001860B63}>)
1: (TRIVIAL-BACKTRACE:PRINT-BACKTRACE #<SIMPLE-ERROR "Hello world!" {1002D9CE23}> :OUTPUT NIL :IF-EXISTS :APPEND :VERBOSE NIL)
2: ((FLET "H0" :IN TRY-HANDLER-BIND) #<SIMPLE-ERROR "Hello world!" {1002D9CE23}>)
3: (SB-KERNEL::%SIGNAL #<SIMPLE-ERROR "Hello world!" {1002D9CE23}>)
4: (ERROR "Hello world!")
5: (DO-SOMETHING)
6: (TRY-HANDLER-BIND)
7: (SB-INT:SIMPLE-EVAL-IN-LEXENV (TRY-HANDLER-BIND) #<NULL-LEXENV>)
8: (EVAL (TRY-HANDLER-BIND))

Printing the backtrace of where the error was signaled is certainly more useful than printing the backtrace of where it was handled.

This post doesn't exhibit all practical differences between both operators. I hope that it will be useful for some of you. Cheers!