ERRORS & CONDITIONS
In the next chapter we'll cover macros–Common Lisp's most famous and powerful feature. But Lisp's conditional system is competitive for the next more powerful feature, and it's the feature we'll cover in this chapter.
Signaling Conditions
In Lisp, instead of "throwing" or "returning" an error, you "signal a
condition". The most common way to signal some condition is to use error or
cerror.
(defun oops (x)
(when (zerop x)
(error "What the h?")))
That opens the debugger:
(oops 0)
ResultWhat the h?
[Condition of type SIMPLE-ERROR]
Restarts:
0: [*ABORT] Return to SLY's top level.
1: [ABORT] abort thread (#<THREAD tid=12411 "slynk-worker" RUNNING {7007185183}>)
Notice that the condition is a SIMPLE-ERROR.
cerror will create a continuable error.
(defun oops (x)
(when (zerop x)
(cerror "Want to continue?" "You passed ~d" x))
(print "You continued."))
Calling it:
(oops 0)
ResultYou passed 0
[Condition of type SIMPLE-ERROR]
Restarts:
0: [CONTINUE] Want to continue?
1: [*ABORT] Return to SLY's top level.
2: [ABORT] abort thread (#<THREAD tid=12483 "slynk-worker" RUNNING {700776ADA3}>)
Continuing will continue after the point of the error.
If you only want to make a warning, you can use warn.
(progn
(warn "You are about to be Rick Rolled.")
(format t "Never Gonna Give You Up")
(error "You've been deserted.")
(format t "I hate meta."))
If you execute this, it will print the warning and the first format in the
REPL and then open a debugger with the error message. You won't see the second
format message.
With assert, you can check a condition. If it returns t, the program
continues. If the condition returns nil, it will signal a continuable error
and then you can assign a value to the selected symbols and retry the assert.
(defun using-assertion (x)
(print "before assert")
(assert (> x 0)
(x)
"~d Needs to be a positive number." x)
(print "after assert")
(print x))
Calling it:
(using-assertion -4)
ResultNeeds to be a positive number.
[Condition of type SIMPLE-ERROR]
Restarts:
0: [CONTINUE] Retry assertion with new value for X.
1: [*ABORT] Return to SLY's top level.
2: [ABORT] abort thread (#<THREAD tid=12435 "slynk-worker" RUNNING {7007A4FF33}>)
If you choose CONTINUE, then you'll be prompted for a new value.
Defining Conditions
If you need or want to provide more control over error handling, you can define your own conditions.
(define-condition naughty-word (error)
()
(:documentation "A condition for when naughty words are detected."))
Conditions are objects using the CLOS.
(make-condition 'naughty-word)
Result#<NAUGHTY-WORD {7009850503}>
Let's make a list of naughty words to detect and a function to detect them.
(defparameter *naughty-words* '(rust java haskell ruby python crypto bitcoin)
"Words that must never be uttered.")
(defun naughty-word-p (word)
"Check if a word is in *naughty-words*."
(member word *naughty-words*))
(defun process-word (word)
"Signal a naughty-word condition if a word is naughty."
(when (naughty-word-p word)
(error 'naughty-word))
word)
(defun process-sentence (sentence)
"Look through all the words in a sentence and do something if any of them are
naughty."
(loop :for word :in sentence
:collect (process-word word)))
(process-sentence '(java has the best oop system among all programming languages))
When you run the above code, the debugger will pop up with an error:
Condition ALMIGHTY/KAIKEI/TESTS::NAUGHTY-WORD was signalled.
[Condition of type NAUGHTY-WORD]
Restarts:
0: [*ABORT] Return to SLY's top level.
1: [ABORT] abort thread (#<THREAD tid=12475 "slynk-worker" RUNNING {70069EF533}>)
We can improve our error messages by modifying the condition definition.
(define-condition naughty-word (simple-error)
((bad-word :initarg :bad-word
:initform nil
:reader bad-word))
(:documentation "A condition for when naughty words are detected.")
(:report (lambda (c stream)
(let ((word (bad-word c)))
(format stream "Naughty word detected: ~a" word)))))
Notice that after the slots (we only have one: bad-word) come the options,
each passed as plists.
Then we update the call to error:
(defun process-word (word)
"Signal a naughty-word condition if a word is naughty."
(when (naughty-word-p word)
(error 'naughty-word :bad-word word))
word)
(process-sentence '(java has the best oop system among all programming languages))
Run process-sentence again and your debugger will display the following
message:
Naughty bad-word detected: JAVA
[Condition of type NAUGHTY-WORD]
Restarts:
0: [RETRY] Retry SLY evaluation request.
1: [*ABORT] Return to SLY's top level.
2: [ABORT] abort thread (#<THREAD tid=21471 "slynk-worker" RUNNING {700CB7C5F3}>)
Backtrace:
0: (PROCESS-WORD JAVA)
1: (PROCESS-SENTENCE (JAVA HAS THE BEST OOP SYSTEM ...))
Restarts
If you define a condition, it's usually because you want to provide the ability
to recover from some error. One recovery mechanism you can provide are
restarts. We've seen restarts many times inside the debugger: there are three
restarts in the above debugger message.
To provide more restarts, use restart-case.
(defun replace-word (word)
"Replace a word in a list."
(intern (string-upcase (read-line))))
(defun process-word (word)
"Check a word. If it's naughty, give the user the choice to replace the word."
(if (naughty-word-p word)
(restart-case
(error 'naughty-word :bad-word word)
(continue ()
:report "Sticks and stones. Just leave the word as it is."
word)
(censor ()
:report "Replace the naughty word with one you like more."
(replace-word word)))
word))
(process-sentence '(java has the best oop system among all programming languages))
ResultNaughty bad-word detected: <unknown>
[Condition of type NAUGHTY-WORD]
Restarts:
0: [CONTINUE] Sticks and stones. Just leave the word as it is.
1: [CENSOR] Replace the naughty word with one you like more.
2: [RETRY] Retry SLY evaluation request.
3: [*ABORT] Return to SLY's top level.
4: [ABORT] abort thread (#<THREAD tid=21479 "slynk-worker" RUNNING {700CC9EDD3}>)
Now when you run process-sentence, you can replace any of the words found in
the *naughty-words* list using a restart.
Handling Conditions
Another recovery mechanism is via "handling". When the naughty-word condition
is signaled, you can prevent the program from stopping and the debugger from
opening by using handler-case.
(defun process-sentence (sentence)
"Look through all the words in a sentence and do something if any of them are
naughty."
(loop :for word :in sentence
:collect (handler-case (process-word word)
(naughty-word () '***))))
(process-sentence '(java has the best oop system among all programming languages))
Result(*HAS THE BEST OOP SYSTEM AMONG ALL PROGRAMMING LANGUAGES)
Now, instead of bringing up the debugger, if a naughty word is encountered during the iteration, it will simply be replaced by "*".
Preserving Local State
Being able to signal a condition, and then choose how to response based on the condition is very nice, but Lisp has one more trick up its sleeve.
handler-case has a problem: it unwinds the stack. That means that you lose
local state at the point where you decide to do something with a naughty-word
condition. In the simple example above, it's difficult to understand what that
looks like. To illustrate the problem with handler-case, we'll consider a
different problem: working with a stream of data.
Let's say we have a simple key=value style configuration system and a file like this:
environment=production
debug=false
api_key+oh-dear
db=sqlite
db_username=admin
db_password:pass
some other broken stuff
We see a few errors. While parsing this file, we'd like to signal a condition and recover, report which lines are broken, what the broken lines look like, etc. and continue reading the rest of the file.
Let's begin by parsing the file:
(defun parse-line (line)
"Parse a 'key=value' line into a cons pair."
(let ((pos (position #\= line)))
(when pos
(cons (subseq line 0 pos)
(subseq line (1+ pos))))))
(defun read-config (stream)
"Read config lines from a stream, collecting key=value pairs."
(loop :for line = (read-line stream nil nil)
:for line-number :from 1
:while line
:for parsed = (parse-line line)
:if parsed
:collect parsed
:else
:do (restart-case
(error 'malformed-line
:line line
:line-number line-number)
(skip-line ()
:report "Skip this line and continue."))))
We use loop to read from each line and to count line-numbers up from 1. If we
hit a line we can't parse, we signal a malformed-line condition, providing the user
the skip-line restart in the debugger.
The malformed-line condition looks like this:
(define-condition malformed-line (error)
((line :initarg :line
:reader malformed-line-text)
(line-number :initarg :line-number
:reader malformed-line-number))
(:report (lambda (c stream)
(format stream "Malformed line ~d: ~a"
(malformed-line-number c)
(malformed-line-text c)))))
When this condition is signaled, it will tell us both the line number and content of the broken line.
Let's try it out as is:
(with-open-file (stream #P"my.config" :direction :input
:element-type 'character)
(read-config stream))
ResultMalformed line 3: apikey+oh-dear
[Condition of type MALFORMED-LINE]
Restarts:
0: [SKIP-LINE] Skip this line and continue.
1: [RETRY] Retry SLY evaluation request.
2: [*ABORT] Return to SLY's top level.
3: [ABORT] abort thread (#<THREAD tid=14415 "slynk-worker" RUNNING {70054C5033}>)
All is well: that is the broken line and its contents. If you choose to skip the line, it will continue parsing and give us the next broken line, and so on, and then finally the not-broken parts of the configuration.
((environment . production) (debug . false) (db . sqlite) (dbusername. admin))
(defun load-config-case (path)
(with-open-file (stream path)
(handler-case (read-config stream)
(malformed-line (c)
(format t "Gave up at line ~d.~%" (malformed-line-number c))
nil))))
(load-config-case #P"my.config")
ResultGave up at line 3.
=> NIL
You don't have access to the skip-line restart, since that's established in the
read-config loop. You've broken out of the loop and can't get back in. The
stack has unwound.
That's where handler-bind and invoke-restart come to the rescure. Unlike
handler-case, handler-bind can catch that malformed-line condition and
then invoke the skip-line restart.
(defun load-config-bind (path)
(with-open-file (stream path)
(handler-bind
((malformed-line
(lambda (c)
(format t "Skipping line ~d.~%" (malformed-line-number c))
(invoke-restart 'skip-line))))
(read-config stream))))
(load-config-bind #P"my.config")
Result((environment . production) (debug . false) (db . sqlite) (dbusername. admin))
In this case, the debugger never popped up. Instead of manually choosing the
restart for the broken lines, we used invoke-restart to skip them for us.
What if we try to call invoke-restart inside the handler-case version?
(defun load-config-case (path)
(with-open-file (stream path)
(handler-case (read-config stream)
(malformed-line (c)
(format t "Gave up at line ~d.~%" (malformed-line-number c))
(invoke-restart 'skip-line)))))
(load-config-case #P"my.config")
ResultNo restart SKIP-LINE is active.
[Condition of type SB-INT:SIMPLE-CONTROL-ERROR]
Restarts:
0: [RETRY] Retry SLY evaluation request.
1: [*ABORT] Return to SLY's top level.
2: [ABORT] abort thread (#<THREAD tid=14387 "slynk-worker" RUNNING {7007B59BA3}>)
Won't work. The stack has been unwound, so we no longer have access to the
skip-line restart. And you need restarts to be able to return to the loop
iteration. If you used a skip-line function in handler-bind, you would end
the process in a different frame. invoke-restart is how you restart the
process at the point where the condition was signaled.
You can also
Useful Variables & Functions
If you want the program to stop just before the error ("break"), you can
modify the *break-on-signals* variable.
The *break-on-signals* variable is set to nil by default, but when set to some
condition or conditions, will break just before the condition is signaled. It's
useful for getting a look at the state of the program just before a condition is
signaled, especially when you don't know exactly where the error is coming from
(in which case, you could use Sly stickers + breaking as I wrote about earlier
in THE LISP IDE/STICKERS).

