In this chapter you'll learn how to create your own error detection, reporting and recovery mechanisms. A good error handling strategy can give your program the ability to gracefully handle both expected and unexpected errors without failing or losing critical data.
One of the most common failings of computer programs is the failure to report failures in a meaningful way. If some input is out of the expected range, or if a calculation exceeds the capabilities of the program, or if communication does not succeed with some external device, a poorly-written program will simply "roll over and die" with a cryptic error message related to hidden details of the program's implementation. In theory, it's nice to be able to construct programs without limits; the dynamic nature of Lisp certainly enables this practice.
But in almost every non-trivial program there will always arise
some fatal situation that can be anticipated by the programmer but
not addressed by the program. It is precisely for these situations
that Lisp provides the ERROR
function.
ERROR
expects a format string and arguments. (We've
seen FORMAT
briefly in Chapter
4, and will examine it in detail in Chapter 24.) ERROR
gives your
program a standard way to announce a fatal error. You simply compose
an appropriate message using the format string and (optional)
arguments, and ERROR
takes care of the rest.
? (defun divide (numerator denominator) (when (zerop denominator) (error "Sorry, you can't divide by zero.")) (/ numerator denominator)) DIVIDE ? (divide 4 3) 4/3 ? (divide 1 0) Error: Sorry, you can't divide by zero.
Your program never returns from the call to ERROR
.
Instead, the Lisp debugger will be entered. You'll have an
opportunity to examine the cause of the error while in the debugger,
but you will not be able to resume your program's execution. This
makes ERROR
a rather extreme response to a problem detected
by your program. Later, we'll see how to report problems
and give the user an opportunity to correct the problem.
We'll even see how errors can be handled automatically.
Note: If you have a really old Lisp system, it may not include an implementation of conditions. If so, this section and the following one may not be of much use to you, except to point out what your Lisp system lacks as compared to the current standard.
An error is just a condition that requires some kind of correction before your program may continue. The error may be corrected by having the program's user interact with the debugger, or through the intervention of a handler (as we'll see later in this chapter).
A condition is just some exceptional event that happens in your program. The event may be due to an error, or it could be something of interest that happens while your program runs. For example, a program that writes entries to a log file on disk might call a routine that handles the record formatting and writing. The logging routine might periodically check the amount of space available on disk and signal a condition when the disk becomes ninety percent full. This is not an error, because the logger won't fail under this condition. If your program ignores the "almost-full" message from the logger, nothing bad will happen. However, your program may wish to do something useful with the information about the available disk space, such as archiving the log to a different device or informing the user that some corrective action may be needed soon.
Now we know the distinction between conditions and errors. For now, we're going to focus our attention on the tools Lisp provides for handling errors. Later, we'll look at how your program can signal and handle conditions.
You could report errors using format strings as described above. But for the sake of consistency and maintainability, you'll probably want to create different classifications of errors. That way, you can change the presentation of an entire class of errors without searching your program code to change all of the similar format strings.
A condition
represents some exceptional
situation which occurs during the execution of your program. An
error is a kind of condition, but not all conditions are errors. The
next section will cover this distinction in greater detail.
You can use DEFINE-CONDITION
to create type hierarchies
for conditions in much the same way that you use DEFCLASS
to create type hierarchies for your program's data.
? (define-condition whats-wrong (error) ((what :initarg :what :initform "something" :reader what)) (:report (lambda (condition stream) (format stream "Foo! ~@(~A~) is wrong." (what condition)))) (:documentation "Tell the user that something is wrong.")) WHATS-WRONG ? (define-condition whats-wrong-and-why (whats-wrong) ((why :initarg :why :initform "no clue" :reader why)) (:report (lambda (condition stream) (format stream "Uh oh! ~@(~A~) is wrong. Why? ~@(~A~)." (what condition) (why condition))))) WHATS-WRONG-AND-WHY ? (error 'whats-wrong-and-why) Error: Uh oh! Something is wrong. Why? No clue. ? (error 'whats-wrong-and-why :what "the phase variance" :why "insufficient tachyon flux") Error: Uh oh! The phase variance is wrong. Why? Insufficient tachyon flux. ? (define-condition whats-wrong-is-unfathomable (whats-wrong-and-why) () (:report (lambda (condition stream) (format stream "Gack! ~@(~A~) is wrong for some inexplicable reason." (what condition))))) WHATS-WRONG-IS-UNFATHOMABLE ? (error 'whats-wrong-is-unfathomable) Error: Gack! Something is wrong for some inexplicable reason.
As you can see, conditions have parents, slots and options just
like classes. The :REPORT
option is used to generate
the textual presentation of a condition. The
:DOCUMENTATION
option is for the benefit of the
programmer; you can retrieve a condition's documentation using
(DOCUMENTATION 'condition-name 'type)
.
ANSI Common Lisp also allows a:DEFAULT-INITARGS
option. Some Lisp systems still base their implementation of conditions on the description found in Guy Steele's "Common Lisp: The Language, 2nd Edition" (CLtL2); these implementations do not have a:DEFAULT-INITARGS
option.
If you've compared the ERROR
calls in this section
to those of the previous section, you're probably wondering how both
a string and a symbol can designate a condition. If you pass a
symbol to ERROR
, it constructs a condition using
MAKE-CONDITION
(analogous to MAKE-INSTANCE
for CLOS objects); the symbol designates the type of the condition,
and the arguments are used to initialize the condition. If you pass
a format string to ERROR
, the format string and its
arguments become initialization options for the construction of a
condition of type SIMPLE-ERROR
.
Of course, you can also pass an instantiated condition object to
ERROR
:
? (let ((my-condition (make-condition 'simple-error :format-control "Can't do ~A." :format-arguments '(undefined-operation)))) (error my-condition)) Error: Can't do UNDEFINED-OPERATION.
Lisp systems designed according to CLtL2 will use:FORMAT-STRING
in place of:FORMAT-CONTROL
.
In this final section, we'll see how to recover from errors. The
simplest forms involve the use of CERROR
or ASSERT
.
? (progn (cerror "Go ahead, make my day." "Do you feel lucky?") "Just kidding") Error: Do you feel lucky? Restart options: 1: Go ahead, make my day. 2. Top level
The "Restart options" list shown in this and the following examples is typical, but not standard. Different Lisp systems will present restart information in their own ways, and may add other built in options.
CERROR
has two required arguments. The first
argument is a format control string that you'll use to tell the
program's user what will happen upon continuing
from
the error. The second argument is a condition designator (a format
control string, a symbol that names a condition, or a condition
object -- see above) used to
tell the program's user about the error.
The rest of CERROR
's arguments, when present, are
used by the the format control strings and -- when the
second argument is a symbol that names a condition type -- as
keyword arguments to MAKE-CONDITION
for that type.
In either case, you have to construct the format control strings
so that they address the proper arguments. The FORMAT
directive ~
n*
can be used to skip
n arguments (n is 1 if omitted).
? (defun expect-type (object type default-value) (if (typep object type) object (progn (cerror "Substitute the default value ~2*~S." "~S is not of the expected type ~S." object type default-value) default-value))) EXPECT-TYPE ? (expect-type "Nifty" 'string "Bear") "Nifty" ? (expect-type 7 'string "Bear") Error: 7 is not of the expected type STRING. Restart options: 1: Substitute the default value "Bear". 2. Top level ? 1 "Bear"
Notice how the first format control string uses only the third
format argument: DEFAULT-VALUE
. It skips the first two
format arguments with the ~2*
directive. You do similar
things if the arguments are keyword initializer arguments when you
provide a symbol as the second argument to CERROR
; the
only difference is that you have to count the keywords
and
the values when deciding how many arguments to
skip. Here's the previous example, written with a designator for a
condition of type EXPECT-TYPE-ERROR
instead of a format
control string. Note how we skip five arguments to get to the
DEFAULT-VALUE
. Note also the use of
:ALLOW-OTHER-KEYS T
, which permits us to add the
:IGNORE DEFAULT-VALUE
keyword argument which is not
expected as an initialization argument for the
EXPECT-TYPE-ERROR
condition; without this, we'd get an
error for the unexpected keyword argument.
? (define-condition expect-type-error (error) ((object :initarg :object :reader object) (type :initarg :type :reader type)) (:report (lambda (condition stream) (format stream "~S is not of the expected type ~S." (object condition) (type condition))))) EXPECT-TYPE-ERROR ? (defun expect-type (object type default-value) (if (typep object type) object (progn (cerror "Substitute the default value ~5*~S." 'expect-type-error :object object :type type :ignore default-value :allow-other-keys t) default-value))) EXPECT-TYPE ? (expect-type "Nifty" 'string "Bear") "Nifty" ? (expect-type 7 'string "Bear") Error: 7 is not of the expected type STRING. Restart options: 1: Substitute the default value "Bear". 2. Top level ? 1 "Bear"
ASSERT
is ideal for those situations where your
program's state must pass some test -- an assertion. In its
simplest form, ASSERT
does only that.
? (defun my-divide (numerator denominator) (assert (not (zerop denominator))) (/ numerator denominator)) MY-DIVIDE ? (my-divide 3 0) Error: Failed assertion (NOT (ZEROP DENOMINATOR)) Restart options: 1. Retry the assertion 2. Top level
This report is correct, but nor particularly useful; your program
would have signalled a DIVISION-BY-ZERO
error without the
ASSERT
. What would be helpful is the ability
to correct the offending value -- the zero denominator, in this case --
and continue from the error. ASSERT
's optional second argument
lets you list places whose values you might want to change to correct
the problem.
? (defun my-divide (numerator denominator) (assert (not (zerop denominator)) (numerator denominator)) (/ numerator denominator)) MY-DIVIDE ? (my-divide 3 0) Error: Failed assertion (NOT (ZEROP DENOMINATOR)) Restart options: 1. Change the values of some places, then retry the assertion 2. Top level ? 1 Value for NUMERATOR: 3 Value for DENOMINATOR 0.5 6.0
Of course, the choice of values to set is up to you. I used both
NUMERATOR
and DENOMINATOR
in the example to
emphasize the fact that the list of places does not have to be just
the variables tested in the assertion. (However, at least one
of the places must affect the result of the assertion.)
One last refinement to ASSERT
lets you specify your own
message to use when an assertion fails. By default, ASSERT
may
display the test form, but it is not required to do so. By specifying
a condition designator and arguments following the list of places, you
can be assured that you know what message will be printed upon an
assertion failure.
? (defun my-divide (numerator denominator) (assert (not (zerop denominator)) (numerator denominator) "You can't divide ~D by ~D." numerator denominator) (/ numerator denominator)) MY-DIVIDE ? (my-divide 3 0) Error: You can't divide 3 by 0. Restart options: 1. Change the values of some places, then retry the assertion 2. Top level ? 1 Value for NUMERATOR: 3 Value for DENOMINATOR 2 3/2
You can use HANDLER-BIND
and SIGNAL
to
process exceptions in your program. Here's an extended example based
upon this chapter's earlier description of how a program might use
conditions to report on disk space availability.
? (define-condition high-disk-utilization () ((disk-name :initarg :disk-name :reader disk-name) (current :initarg :current :reader current-utilization) (threshold :initarg :threshold :reader threshold)) (:report (lambda (condition stream) (format stream "Disk ~A is ~D% full; threshold is ~D%." (disk-name condition) (current-utilization condition) (threshold condition))))) HIGH-DISK-UTILIZATION ? (defun get-disk-utilization (disk-name) ;; for this example, we'll just return a fixed value 93) GET-DISK-UTILIZATION ? (defun check-disk-utilization (name threshold) (let ((utilization (disk-utilization name))) (when (>= utilization threshold) (signal 'high-disk-utilization :disk-name name :current utilization :threshold threshold)))) CHECK-DISK-UTILIZATION ? (defun log-to-disk (record name) (handler-bind ((high-disk-utilization #'(lambda (c) (when (y-or-n-p "~&~A Panic?" c) (return-from log-to-disk nil))))) (check-disk-utilization name 90) (print record)) t) LOG-TO-DISK ? (log-to-disk "Hello" 'disk1) Disk DISK1 is 93% full; threshold is 90%. Panic? (y or n) n "Hello" T ? (log-to-disk "Goodbye" 'disk1) Disk DISK1 is 93% full; threshold is 90%. Panic? (y or n) y NIL ? (check-disk-utilization 'disk1 90) NIL
Notice that the condition signalled by
CHECK-DISK-UTILIZATION
has an effect only when a
handler is established for the HIGH-DISK-UTILIZATION
condition. Because of this, you can write exception signalling code
without foreknowledge that the client will provide a handler. This
is most useful when the exception provides information about the
running program, but is not an error if left unhandled.
In the next example, we'll extend the restart options available
to CERROR
. RESTART-BIND
defines, for each
new restart, the message to be printed by the restart user interface
and a function to be executed when the user chooses the restart.
? (define-condition device-unresponsive () ((device :initarg :device :reader device)) (:report (lambda (condition stream) (format stream "Device ~A is unresponsive." (device condition))))) DEVICE-UNRESPONSIVE ? (defun send-query (device query) (format t "~&Sending ~S ~S~%" device query)) SEND-QUERY ? (defun accept-response (device) ;; For the example, the device always fails. nil) ACCEPT-RESPONSE ? (defun reset-device (device) (format t "~&Resetting ~S~%" device)) RESET-DEVICE ? (defun query-device (device) (restart-bind ((nil #'(lambda () (reset-device device)) :report-function #'(lambda (stream) (format stream "Reset device."))) (nil #'(lambda () (format t "~&New device: ") (finish-output) (setq device (read))) :report-function #'(lambda (stream) (format stream "Try a different device."))) (nil #'(lambda () (return-from query-device :gave-up)) :report-function #'(lambda (stream) (format stream "Give up.")))) (loop (send-query device 'query) (let ((answer (accept-response device))) (if answer (return answer) (cerror "Try again." 'device-unresponsive :device device)))))) QUERY-DEVICE ? (query-device 'foo) Sending FOO QUERY Error: Device FOO is unresponsive. Restart options: 1. Try again. 2. Reset device. 3. Try a different device. 4. Give up. 5. Top level ? 1 Sending FOO QUERY Error: Device FOO is unresponsive. Restart options: 1. Try again. 2. Reset device. 3. Try a different device. 4. Give up. 5. Top level ? 2 Resetting FOO Restart options: 1. Try again. 2. Reset device. 3. Try a different device. 4. Give up. 5. Top level ? 1 Sending FOO QUERY Error: Device FOO is unresponsive. Restart options: 1. Try again. 2. Reset device. 3. Try a different device. 4. Give up. 5. Top level ? 3 New device: bar Restart options: 1. Try again. 2. Reset device. 3. Try a different device. 4. Give up. 5. Top level ? 1 Error: Device FOO is unresponsive. Restart options: 1. Try again. 2. Reset device. 3. Try a different device. 4. Give up. 5. Top level ? 4 :GAVE-UP
The "Try again" restart is established by the CERROR
form; selecting this restart lets the program continue from the
CERROR
form. The "Reset device", "Try a different
device", and "Give up" restarts are created within the
HANDLER-BIND
form; choosing one of these executes the
associated function. Of the restarts defined within the
HANDLER-BIND
, only the "Give up" restart transfers
control out of the CERROR
form -- the others return
control to CERROR
to again display the menu of restart
choices.
Now you've seen the basics of condition handlers and restarts. Lisp has additional built-in abstractions that extend these concepts. If you're interested, you should consult a Common Lisp reference.
There's one last thing you should know about handling conditions.
As we saw earlier, ERROR
causes your program to stop in
the Lisp debugger. You can't continue past the call to
ERROR
, but most Lisp systems will let you back up,
correct the problem that caused the error, and rerun that portion of
the program. If you do the right thing, your program won't call
ERROR
again. This is an amazingly powerful tool to use
during program development. But you don't want to expose your users
to that kind of experience -- they won't be as impressed by the Lisp
debugger as you are.
To protect your users from the debugger, you can wrap portions of
your program in an IGNORE-ERRORS
form.
? (ignore-errors (error "Something bad has happened.") (print "Didn't get here.")) NIL #<SIMPLE-ERROR #x42B26B6> ? (ignore-errors (* 7 9)) 63
If an error occurs within an IGNORE-ERRORS
form, program
execution ends at that point, and IGNORE-ERRORS
returns two
values: NIL
and the condition signalled by ERROR
.
You should use IGNORE-ERRORS
judiciously. Use it only to
wrap forms for which you can't otherwise provide handlers. Note, too, that
the values returned by IGNORE-ERRORS
are not very informative.
But you can decode the second return value to print the actual error
message.
? (defmacro report-error (&body body) (let ((results (gensym)) (condition (gensym))) `(let ((,results (multiple-value-list (ignore-errors ,@body)))) (if (and (null (first ,results)) (typep (second ,results) 'condition) (null (nthcdr 2 ,results))) (let ((,condition (second ,results))) (typecase ,condition (simple-condition (apply #'format t (simple-condition-format-control ,condition) (simple-condition-format-arguments ,condition))) (otherwise (format t "~A error." (type-of ,condition)))) (values)) (values-list ,results))))) REPORT-ERROR ? (report-error (error "I feel like I'm losing my mind, Dave.")) I feel like I'm losing my mind, Dave. ? (report-error (+ 1 no-variable-by-this-name)) UNBOUND-VARIABLE error. ? (report-error (* 7 'f)) TYPE-ERROR error. ? (report-error (let ((n 1)) (/ 8 (decf n)))) DIVISION-BY-ZERO error. ? (report-error (* 2 pi)) ; not an error 6.283185307179586 ? (report-error (values 1 2 3 4)) ; not an error