6.11.8.3 Throw and Catch

Guile only adopted with-exception-handler and raise-exception as its primary exception-handling facility in 2019. Before then, exception handling was fundamentally based on three other primitives with a somewhat more complex interface: catch, with-throw-handler, and throw.

Scheme Procedure: catch key thunk handler [pre-unwind-handler]
C Function: scm_catch_with_pre_unwind_handler (key, thunk, handler, pre_unwind_handler)
C Function: scm_catch (key, thunk, handler)

Establish an exception handler during the dynamic extent of the call to thunk. key is either #t, indicating that all exceptions should be handled, or a symbol, restricting the exceptions handled to those having the key as their exception-kind.

If thunk executes normally, meaning without throwing any exceptions, the handler procedures are not called at all and the result of the thunk call is the result of the catch. Otherwise if an exception is thrown that matches key, handler is called with the continuation of the catch call.

Given the discussion from the previous section, it is most precise and concise to specify what catch does by expressing it in terms of with-exception-handler. Calling catch with the three arguments is the same as:

(define (catch key thunk handler)
  (with-exception-handler
   (lambda (exn)
     (apply handler (exception-kind exn) (exception-args exn)))
   thunk
   #:unwind? #t
   #:unwind-for-type key))

By invoking with-exception-handler with #:unwind? #t, catch sets up an escape continuation that will be invoked in an exceptional situation before the handler is called.

If catch is called with four arguments, then the use of thunk should be replaced with:

   (lambda ()
     (with-throw-handler key thunk pre-unwind-handler))

As can be seen above, if a pre-unwind-handler is passed to catch, it’s like calling with-throw-handler inside the body thunk.

with-throw-handler is the second of the older primitives, and is used to be able to intercept an exception that is being thrown before the stack is unwound. This could be to clean up some related state, to print a backtrace, or to pass information about the exception to a debugger, for example.

Scheme Procedure: with-throw-handler key thunk handler
C Function: scm_with_throw_handler (key, thunk, handler)

Add handler to the dynamic context as a throw handler for key key, then invoke thunk.

It’s not possible to exactly express with-throw-handler in terms of with-exception-handler, but we can get close.

(define (with-throw-handler key thunk handler)
  (with-exception-handler
   (lambda (exn)
     (when (or (eq? key #t) (eq? key (exception-kind exn)))
       (apply handler (exception-kind exn) (exception-args exn)))
     (raise-exception exn))
   thunk))

As you can see, unlike in the case of catch, the handler for with-throw-handler is invoked within the continuation of raise-exception, before unwinding the stack. If the throw handler returns normally, the exception will be re-raised, to be handled by the next exception handler.

The special wrinkle of with-throw-handler that can’t be shown above is that if invoking the handler causes a raise-exception instead of completing normally, the exception is thrown in the original dynamic environment of the raise-exception. Any inner exception handler will get another shot at handling the exception. Here is an example to illustrate this behavior:

(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.

Finally, we get to throw, which is the older equivalent to raise-exception.

Scheme Procedure: throw key arg …
C Function: scm_throw (key, args)

Raise an exception with kind key and arguments args. key is a symbol, denoting the “kind” of the exception.

Again, we can specify what throw does by expressing it in terms of raise-exception.

(define (throw key . args)
  (raise-exception (make-exception-from-throw key args)))

At this point, we should mention the primitive that manage the relationship between structured exception objects throw.

Scheme Procedure: make-exception-from-throw key args

Create an exception object for the given key and args passed to throw. This may be a specific type of exception, for example &programming-error; Guile maintains a set of custom transformers for the various key values that have been used historically.

Scheme Procedure: exception-kind exn

If exn is an exception created via make-exception-from-throw, return the corresponding key for the exception. Otherwise, unless exn is an exception of a type with a known mapping to throw, return the symbol %exception.

Scheme Procedure: exception-args exn

If exn is an exception created via make-exception-from-throw, return the corresponding args for the exception. Otherwise, unless exn is an exception of a type with a known mapping to throw, return (list exn).