A curious case of HANDLER-CASE
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 returnsnil
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!