Chapter 9 - Introducing Error Handling and Non-Local Exits

In this chapter we'll see some of the specialized control flow forms provided by Common Lisp.

UNWIND-PROTECT: When it absolutely, positively has to run

One of the challenges of writing robust programs is to make sure that important parts of your code always run, even in the presence of errors. Usually, this is most important when you're allocating, using and releasing resources such as files and memory, like this:

; Setup 
Allocate some resources
Open some files
; Process 
Process using files and storage (may fail)
; Cleanup 
Close the files
Release the resources

If the processing step might fail (or be interrupted by the user) you should make sure that every possible exit path still goes through the cleanup section to close the files and release the storage. Better still, your program should be prepared to handle errors that occur during the setup phase as you allocate storage and open files, since any of these operations might also fail; any partially completed setup should still be undone in the cleanup section.

Lisp's UNWIND-PROTECT form makes this especially easy to do.

(let (resource stream)
  (unwind-protect
    (progn
      (setq resource (allocate-resource)
            stream (open-file))
      (process stream resource))
    (when stream (close stream))
    (when resource (deallocate resource))))

Here's what happens. The LET binds RESOURCE and STREAM to NIL -- we'll use the NIL value to mean that there has been no resource allocated or file opened. The first form in the UNWIND-PROTECT is a "protected" form; if control leaves the protected form via any means, then the rest of the forms -- the "cleanup" forms -- are guaranteed to be executed.

In our example, the protected form is a PROGN that calls ALLOCATE-RESOURCE and OPEN-FILE to set our local variables, then PROCESS uses these resources. SETQ assigns values sequentially to our local variables: (ALLOCATE-RESOURCE) must succeed before a value can be assigned to RESOURCE, then OPEN-FILE must succeed before its value can be assigned to STREAM. A failure (i.e. an interrupt or error) at any point in this sequence will transfer control out of the protected form.

If the initializations succeed and PROCESS returns normally, control continues into the cleanup forms.

If anything causes the protected form to exit -- for example, an error or an interrupt from the keyboard -- control is transferred immediately to the first cleanup form. The cleanup forms are guarded by WHEN clauses so we won't try to close the stream or deallocate the resource if an error caused them to never be created in the first place.

Gracious exits with BLOCK and RETURN-FROM

The BLOCK and RETURN-FROM forms give you a structured lexical exit from any nested computation. The BLOCK form has a name followed a body composed of zero or more forms. The RETURN-FROM form expects a block name and an optional (the default is NIL) return value.

? (defun block-demo (flag)
    (print 'before-outer)
    (block outer
      (print 'before-inner)
      (print (block inner
               (if flag
                 (return-from outer 7)
                 (return-from inner 3))
               (print 'never-print-this)))
      (print 'after-inner)
      t))
BLOCK-DEMO
? (block-demo t)

BEFORE-OUTER
BEFORE-INNER
7
? (block-demo nil)

BEFORE-OUTER
BEFORE-INNER
3
AFTER-INNER
T

When we call BLOCK-DEMO with T, the IF statement's consequent -- (return-from outer 7) -- immediately returns the value 7 from the (BLOCK OUTER ... form. Calling BLOCK-DEMO with NIL executes the alternate branch of the IF -- (return-from inner 3) -- passing the value 3 to the PRINT form wrapped around the (BLOCK INNER ... form.

Block names have lexical scope: RETURN-FROM transfers control to the innermost BLOCK with a matching name.

Some forms implicitly create a block around their body forms. When a name is associated with the form, such as with DEFUN, the block takes the same name.

? (defun block-demo-2 (flag)
    (when flag
      (return-from block-demo-2 nil))
    t)
BLOCK-DEMO-2
? (block-demo-2 t)
NIL
? (block-demo-2 nil)
T

Other forms, such as the simple LOOP and DOTIMES, establish a block named NIL around their body forms. You can return from a NIL block using (RETURN-FROM NIL ...), or just (RETURN ...).

? (let ((i 0))
    (loop
      (when (> i 5)
        (return))
      (print i)
      (incf i)))

0
1
2
3
4
5
NIL
? (dotimes (i 10)
    (when (> i 3)
      (return t))
    (print i))

0
1
2
3
T

Escape from anywhere (but not at any time) with CATCH and THROW

So BLOCK and RETURN-FROM are handy for tranferring control out of nested forms, but they're only useful when the exit points (i.e. block names) are lexically visible. But what do you do if you want to break out of a chain of function calls?

; WARNING! This won't work! 
(defun bad-fn-a ()
  (bad-fn-b))

(defun bad-fn-b ()
  (bad-fn-c))

(defun bad-fn-c ()
  (return-from bad-fn-a))  ; There is no block BAD-FN-A visible here! 

Enter CATCH and THROW, which let you establish control transfers using dynamic scope. Recall that dynamic scope follows the chain of active forms, rather than the textual enclosure of one form within another of lexical scope.

? (defun fn-a ()
    (catch 'fn-a
      (print 'before-fn-b-call)
      (fn-b)
      (print 'after-fn-b-call)))
FN-A
? (defun fn-b ()
    (print 'before-fn-c-call)
    (fn-c)
    (print 'after-fn-c-call))
FN-B
?(defun fn-c ()
   (print 'before-throw)
   (throw 'fn-a 'done)
   (print 'after-throw))
FN-C
? (fn-a)

BEFORE-FN-B-CALL 
BEFORE-FN-C-CALL 
BEFORE-THROW 
DONE

Making sure files only stay open as long as needed

Opening a file just long enough to process its data is a very common operation. We saw above that UNWIND-PROTECT can be used to ensure that the file gets properly closed. As you might expect, such a common operation has its own form in Lisp.

(with-open-file (stream "file.ext" :direction :input)
  (do-something-with-stream stream))

WITH-OPEN-FILE wraps an OPEN and CLOSE form around the code you provide, and makes sure that the CLOSE gets called at the right time. All of the options available to OPEN may be used in WITH-OPEN-FILE -- I've shown the options you'd use to open a file for input.


Contents | Cover
Chapter 8 | Chapter 10
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.