Common Lisp has mechanisms for error and condition handling as found in other languages, and can do more.
What is a condition ?
Just like in languages that support exception handling (Java, C++, Python, etc.), a condition represents, for the most part, an “exceptional” situation. However, even more so that those languages, a condition in Common Lisp can represent a general situation where some branching in program logic needs to take place, not necessarily due to some error condition. Due to the highly interactive nature of Lisp development (the Lisp image in conjunction with the REPL), this makes perfect sense in a language like Lisp rather than say, a language like Java or even Python, which has a very primitive REPL. In most cases, however, we may not need (or even allow) the interactivity that this system offers us. Thankfully, the same system works just as well even in non-interactive mode.
Let’s dive into it step by step. More resources are given afterwards.
Now best read in the Common Lisp Cookbook.
Credit: our cl-torrents tutorial.
Note: you can contribute any fix or addition to the Cookbook or this page via git ;)
Table of Contents
- Ignore all errors (and return nil)
- Catching any condition - handler-case
- Catching a specific condition
- handler-case VS handler-bind
- Handling conditions - handler-bind
- Creating conditions
- Signaling (throwing) conditions
- Restarts, interactive choices in the debugger
- Run some code, condition or not (“finally”)
- Resources
Ignore all errors (and return nil)
Sometimes you know that a function can fail and you just want to
ignore it: use ignore-errors
:
(ignore-errors
(/ 3 0))
; in: IGNORE-ERRORS (/ 3 0)
; (/ 3 0)
;
; caught STYLE-WARNING:
; Lisp error during constant folding:
; arithmetic error DIVISION-BY-ZERO signalled
; Operation was (/ 3 0).
;
; compilation unit finished
; caught 1 STYLE-WARNING condition
NIL
#<DIVISION-BY-ZERO {1008FF5F13}>
We get a welcome division-by-zero
warning but the code runs well and
it returns two things: nil
and the condition that was signaled. We
could not choose what to return.
Rembember that we can inspect
the condition with a right click in Slime.
Catching any condition - handler-case
ignore-error
is built from handler-case
. We can write the previous
example by catching the general error
but now we can return whatever
we want:
(handler-case (/ 3 0)
(error (c)
(format t "We caught a condition.~&")
(values 0 c)))
; in: HANDLER-CASE (/ 3 0)
; (/ 3 0)
;
; caught STYLE-WARNING:
; Lisp error during constant folding:
; Condition DIVISION-BY-ZERO was signalled.
;
; compilation unit finished
; caught 1 STYLE-WARNING condition
We caught a condition.
0
#<DIVISION-BY-ZERO {1004846AE3}>
We also returned two values, 0 and the signaled condition.
The general form of handler-case
is
(handler-case (code that errors out)
(condition-type (the-condition) ;; <-- optional argument
(code))
(another-condition (the-condition)
...))
We can also catch all conditions by matching t
, like in a cond
:
(handler-case
(progn
(format t "This won't work…~%")
(/ 3 0))
(t (c)
(format t "Got an exception: ~a~%" c)
(values 0 c)))
;; …
;; This won't work…
;; Got an exception: arithmetic error DIVISION-BY-ZERO signalled
;; Operation was (/ 3 0).
;; 0
;; #<DIVISION-BY-ZERO {100608F0F3}>
Catching a specific condition
We can specify what condition to handle:
(handler-case (/ 3 0)
(division-by-zero (c)
(format t "Caught division by zero: ~a~%" c)))
;; …
;; Caught division by zero: arithmetic error DIVISION-BY-ZERO signalled
;; Operation was (/ 3 0).
;; NIL
This workflow is similar to a try/catch as found in other languages, but we can do more.
Ignoring the condition argument
If you don’t access the condition object in your handlers, but you still keep it has an argument for good practice, you’ll see this compiler warning often:
; caught STYLE-WARNING:
; The variable C is defined but never used.
To remove it, use a declare
call as in:
(handler-case (/ 3 0)
(division-by-zero (c)
(declare (ignore c))
(format t "Caught division by zero~%"))) ;; we don't print "c" here and don't get the warning.
handler-case VS handler-bind
handler-case
is similar to the try/catch
forms that we find in
other languages.
handler-bind
(see the next examples), is what to use
when we need absolute control over what happens when a signal is
raised. It allows us to use the debugger and restarts, either
interactively or programmatically.
If some library doesn’t catch 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 whose this library called. And we can see the stack
trace, with every frame that was called and, in some lisps, even see
local variables and such. Once we handler-case
, we “forget” about
this, everything is unwound. handler-bind
does not rewind the
stack.
Handling conditions - handler-bind
Here we use handler-bind
.
Its general form is:
(handler-bind ((a-condition #'function-to-handle-it)
(another-one #'another-function))
(code that can...)
(...error out))
So, our simple example:
(handler-bind
((division-by-zero #'(lambda (c) (format t "hello condition~&"))))
(/ 3 0))
This prints some warnings, then it prints our “hello” and still enters the debugger. If we don’t want to enter the debugger, we have to define a restart and invoke it.
A real example with the
unix-opts
library, that
parses command line arguments. It defined some conditions:
unknown-option
, missing-arg
and arg-parser-failed
, and it is up
to use to write what to do in these cases.
(handler-bind ((opts:unknown-option #'unknown-option)
(opts:missing-arg #'missing-arg)
(opts:arg-parser-failed #'arg-parser-failed))
(opts:get-opts))
Our unknown-option
function is simple and looks like this:
(defun unknown-option (condition)
(format t "~s option is unknown.~%" (opts:option condition))
(opts:describe)
(exit)) ;; <-- we return to the command line, no debugger.
it takes the condition as parameter, so we can read information from
it if needed. Here we get the name of the erronous option with the
defined reader (opts:option condition)
(see below).
Creating conditions
With define-condition
, and we can inherit from error
or simple-error
:
(define-condition my-division-by-zero (error) ())
It is a regular class, so we can add information into slots. Here, we add a custom message:
(define-condition my-division-by-zero (error)
((dividend :initarg :dividend
:reader dividend)) ;; <= so we'll get the dividend with (dividend condition), as soon as on the next line.
;; the :report is the message into the debugger:
(:report (lambda (condition stream) (format stream "You were going to divide ~a by zero.~&" (dividend condition)))))
The general form looks like a regular class definition:
(define-condition my-condition (condition-it-inherits-from)
;; list of arguments, can be "()".
((message :initarg :message
:reader my-condition-message)
(second ...))
;; class arguments
(:report (lambda (condition stream) (...))) ;; what is printed in the REPL.
(:documentation "a string")) ;; good practice ;)
Now when we throw this condition we must pass it an error message (it
is a required argument), and read it with error-message
(the
:reader
).
What’s in :report
will be printed in the REPL.
Let’s try our condition. We define a simple function that checks our divisor, and signals our condition if it is equal to zero:
(defun my-division (x y)
(if (= y 0)
(error 'MY-DIVISION-BY-ZERO :dividend x))
(/ x y))
When we use it, we enter the debugger:
(my-division 3 0)
;;
;; into the debugger:
;;
You were going to divide 3 by zero.
[Condition of type MY-DIVISION-BY-ZERO]
Restarts:
0: [RETRY] Retry SLIME REPL evaluation request.
1: [*ABORT] Return to SLIME's top level.
2: [ABORT] abort thread (#<THREAD "repl-thread" RUNNING {1002957FA3}>)
Backtrace:
0: (MY-DIVISION 3 0)
We can inspect the backtrace, go to the source (v
in Slime), etc.
Here is how unix-opts
defines its unknown-option
condition:
(define-condition troublesome-option (simple-error)
((option
:initarg :option
:reader option))
(:report (lambda (c s) (format s "troublesome option: ~s" (option c))))
(:documentation "Generalization over conditions that have to do with some
particular option."))
(define-condition unknown-option (troublesome-option)
()
(:report (lambda (c s) (format s "unknown option: ~s" (option c))))
(:documentation "This condition is thrown when parser encounters
unknown (not previously defined with `define-opts') option."))
Signaling (throwing) conditions
We can use error
, like we did above. Two ways:
(error "some text")
: signals a condition of typesimple-error
(error 'my-error :message "We did this and this and it didn't work.")
Throwing these conditions will enter the interactive debugger, where a few options will be presented by default. We can give more options with restarts, and we can prevent from entering the debugger by handling the condition and invoking a restart.
Simple example from unix-opts
: it adds information into the option
slot:
(error 'unknown-option
:option opt)
Restarts, interactive choices in the debugger
Defining restarts
Restarts are the choices we get in the debugger, which always has the
RETRY
and ABORT
ones. We can add choices to the top of the list:
(defun division-restarter ()
(restart-case (/ 3 0)
(return-zero () 0)
(divide-by-one () (/ 3 1))))
By calling this stupid function we get two new choices at the top of the debugger:
Note: read in lisper.in’s blogpost on csv parsing (see Resources) how this system was used effectively in production.
But that’s not all, by handling restarts we can start over the operation as if the error didn’t occur (as seen in the stack).
Calling restarts programmatically
With invoke-restart
.
(defun division-and-bind ()
(handler-bind
((error (lambda (c)
(format t "Got error: ~a~%" c) ;; error-message
(format t "and will divide by 1~&")
(invoke-restart 'divide-by-one))))
(division-restarter)))
;; (DIVISION-AND-BIND)
;; Got error: arithmetic error DIVISION-BY-ZERO signalled
;; and will divide by 1
;; Operation was (/ 3 0).
;; 3
Note that we called the form that contains our restarts
(division-restarter
) and not the function that throws the error.
Using other restarts
find-restart 'name-of-restart
will return the most recent bound
restart with the given name, or nil
. We can invoke it with
invoke-restart
.
Prompting the user to enter a new value
Let’s add a restart in our division-restarter
to offer the user to
enter a new dividend, and run the division again.
(defun division-restarter ()
(restart-case (/ 3 0)
(return-nil () nil)
(divide-by-one () (/ 3 1))
(choose-another-dividend (new-dividend)
:report "Please choose another dividend"
:interactive (lambda ()
(format t "Enter a new dividend: ")
(list (read))) ;; <-- must return a list.
(format t "New division: 3/~a = ~a~&" new-dividend (/ 3 new-dividend)))))
We get prompted in the debugger:
arithmetic error DIVISION-BY-ZERO signalled
Operation was (/ 3 0).
[Condition of type DIVISION-BY-ZERO]
Restarts:
0: [RETURN-NIL] RETURN-NIL
1: [DIVIDE-BY-ONE] DIVIDE-BY-ONE
2: [CHOOSE-ANOTHER-DIVIDEND] Please choose another dividend <-- new
3: [RETRY] Retry SLIME REPL evaluation request.
4: [*ABORT] Return to SLIME's top level.
5: [ABORT] abort thread (#<THREAD "repl-thread" RUNNING {1002A47FA3}>)
The new choose-another-dividend
restart takes an argument for the
new dividend, that will be fed by the :interactive
lambda, which
read
s for user input and must return a list.
We use it like this:
(division-restarter)
;;
;; Entered debugger, chose the 2nd restart.
;;
Enter a new dividend: 10 <-- got prompted to enter a new value.
New division: 3/10 = 3/10
NIL
In a real situation we might want to call our “restarter” recursively, to get into the debugger again if we enter a bad value.
Hide and show restarts
Restarts can be hidden. In restart-case
, in addition to :report
and :interactive
, they also accept a :test
key:
(restart-case
(return-zero ()
:test (lambda ()
(some-test))
...
Run some code, condition or not (“finally”)
The “finally” part of others try/catch/finally
forms is done with unwind-protect
.
It is the construct used in “with-” macros, like with-open-file
,
which always closes the file after it.
You’re now more than ready to write some code and to dive into other resources !
Resources
- Practical Common Lisp: “Beyond Exception Handling: Conditions and Restarts” - the go-to tutorial, more explanations and primitives.
- Common Lisp Recipes, chap. 12, by E. Weitz
- language reference
- lisper.in - example with parsing a csv file and using restarts with success, in a flight travel company.
- Condition Handling in the Lisp family of languages
- z0ltan.wordpress.com
- https://github.com/svetlyak40wt/python-cl-conditions - implementation of the CL conditions system in Python.