Chapter 23 - To Err is Expected; To Recover, Divine

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.

Signal your own errors and impress your users

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.

Categorize errors using Conditions

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.

Recover from Conditions using Restarts

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 

Contents | Cover
Chapter 22 | Chapter 24
Copyright © 1995-1999, David B. Lamkins
All Rights Reserved Worldwide

This book may not be reproduced without the written consent of its author. Online distribution is restricted to the author's site.