Next: , Previous: Throw Handlers, Up: Exceptions


5.11.7.4 Catch Without Unwinding

Before version 1.8, Guile's closest equivalent to with-throw-handler was lazy-catch. From version 1.8 onwards we recommend using with-throw-handler because its behaviour is more useful than that of lazy-catch, but lazy-catch is still supported as well.

A lazy catch is used in the same way as a normal catch, with key, thunk and handler arguments specifying the exception type, normal case code and handler procedure, but differs in one important respect: the handler procedure is executed without unwinding the call stack from the context of the throw expression that caused the handler to be invoked.

— Scheme Procedure: lazy-catch key thunk handler
— C Function: scm_lazy_catch (key, thunk, handler)

This behaves exactly like catch, except that it does not unwind the stack before invoking handler. If the handler procedure returns normally, Guile rethrows the same exception again to the next innermost catch, lazy-catch or throw handler. If the handler exits non-locally, that exit determines the continuation.

— C Function: SCM scm_internal_lazy_catch (SCM tag, scm_t_catch_body body, void *body_data, scm_t_catch_handler handler, void *handler_data)

The above scm_lazy_catch takes Scheme procedures as body and handler arguments. scm_internal_lazy_catch is an equivalent taking C functions. See scm_internal_catch (see Catch) for a description of the parameters, the behaviour however of course follows lazy-catch.

Typically handler is used to display a backtrace of the stack at the point where the corresponding throw occurred, or to save off this information for possible display later.

Not unwinding the stack means that throwing an exception that is caught by a lazy-catch is almost equivalent to calling the lazy-catch's handler inline instead of each throw, and then omitting the surrounding lazy-catch. In other words,

     (lazy-catch 'key
       (lambda () ... (throw 'key args ...) ...)
       handler)

is almost equivalent to

     ((lambda () ... (handler 'key args ...) ...))

But why only almost? The difference is that with lazy-catch (as with normal catch), the dynamic context is unwound back to just outside the lazy-catch expression before invoking the handler. (For an introduction to what is meant by dynamic context, See Dynamic Wind.)

Then, when the handler itself throws an exception, that exception must be caught by some kind of catch (including perhaps another lazy-catch) higher up the call stack.

The dynamic context also includes with-fluids blocks (see Fluids and Dynamic States), so the effect of unwinding the dynamic context can also be seen in fluid variable values. This is illustrated by the following code, in which the normal case thunk uses with-fluids to temporarily change the value of a fluid:

     (define f (make-fluid))
     (fluid-set! f "top level value")
     
     (define (handler . args)
       (cons (fluid-ref f) args))
     
     (lazy-catch 'foo
                 (lambda ()
                   (with-fluids ((f "local value"))
                     (throw 'foo)))
                 handler)
     =>
     ("top level value" foo)
     
     ((lambda ()
        (with-fluids ((f "local value"))
          (handler 'foo))))
     =>
     ("local value" foo)

In the lazy-catch version, the unwinding of dynamic context restores f to its value outside the with-fluids block before the handler is invoked, so the handler's (fluid-ref f) returns the external value.

lazy-catch is useful because it permits the implementation of debuggers and other reflective programming tools that need to access the state of the call stack at the exact point where an exception or an error is thrown. For an example of this, see REFFIXME:stack-catch.

It should be obvious from the above that lazy-catch is very similar to with-throw-handler. In fact Guile implements lazy-catch in exactly the same way as with-throw-handler, except with a flag set to say “where there are slight differences between what with-throw-handler and lazy-catch would do, do what lazy-catch has always done”. There are two such differences:

  1. with-throw-handler handlers execute in the full dynamic context of the originating throw call. lazy-catch handlers execute in the dynamic context of the lazy-catch expression, excepting only that the stack has not yet been unwound from the point of the throw call.
  2. If a with-throw-handler handler throws to a key that does not match the with-throw-handler expression's key, the new throw may be handled by a catch or throw handler that is _closer_ to the throw than the first with-throw-handler. If a lazy-catch handler throws, it will always be handled by a catch or throw handler that is higher up the dynamic context than the first lazy-catch.

Here is an example to illustrate the second difference:

     (catch 'a
       (lambda ()
         (with-throw-handler 'b
           (lambda ()
             (catch 'a
               (lambda ()
                 (throw 'b))
               inner-handler))
           (lambda (key . args)
             (throw 'a))))
       outer-handler)

This code will call inner-handler and then continue with the continuation of the inner catch. If the with-throw-handler was changed to lazy-catch, however, the code would call outer-handler and then continue with the continuation of the outer catch.

Modulo these two differences, any statements in the previous and following subsections about throw handlers apply to lazy catches as well.