Google

PLT MzScheme: Language Manual


Exceptions and Control Flow

6.1  Exceptions

MzScheme supports the exception system proposed by Friedman, Haynes, and Dybvig.7 MzScheme's implementation extends that proposal by defining the specific exception values that are raised by each primitive error.

  • (raise exn) raises an exception, where exn represents the exception being raised. The exn argument can be anything; it is passed to the current exception handler. Breaks are disabled while the exception handler is called; see section 6.6 for more information.

  • (current-exception-handler) returns the current exception handler that is used by raise, and (current-exception-handler f) installs the procedure f as the current exception handler. The current-exception-handler procedure is a parameter; see section 7.4.1.7 for more information.

    Any procedure that takes one argument can be an exception handler, but it is an error if the exception handler returns to its caller when invoked by raise. (If an exception handler returns, the current error display handler and current error escape handler are called directly to report the handler's mistake.)

    The default exception handler prints an error message using the current error display handler (see error-display-handler in section 7.4.1.7) and then escapes by calling the current error escape handler (see error-escape-handler in section 7.4.1.7). If an exception is raised while an exception handler is executing, an error message is printed using a primitive error printer and the primitive error escape handler is invoked.

  • (with-handlers ((pred handler) ···) expr ···1) is a syntactic form that evaluates the expr body, installing a new exception handler before evaluating the exprs and restoring the handler when a value is returned (or when control escapes from the expression). The pred and handler expressions are evaluated in the order that they are specified, before the first expr and before the exception handler is changed. The exception handler is installed and restored with parameterize (see section 7.4.2).

    The new exception handler processes an exception only if one of the pred procedures returns a true value when applied to the exception, otherwise the original exception handler is invoked (by raising the exception again). If an exception is handled by one of the handler procedures, the result of the entire with-handlers expression is the return value of the handler.

    When an exception is raised during the evaluation of exprs, each predicate procedure pred is applied to the exception value; if a predicate returns a true value, the corresponding handler procedure is invoked with the exception as an argument. The predicates are tried in the order that they are specified.

    Before any predicate or handler procedure is invoked, the continuation of the entire with-handlers expression is restored. The ``original'' exception handler (the one present before the with-handlers expression was evaluated) is therefore re-installed before any predicate or handler procedure is invoked.

    A particularly useful predicate procedure is not-break-exn?. (not-break-exn? v) returns #f if v is an instance of exn:break (representing an asynchronous break exception), #t otherwise.

The following example defines a divide procedure that returns +inf.0 when dividing by zero instead of signaling an exception (other exceptions raised by / are signaled):

(define div-w-inf 
  (lambda (n d) 
    (with-handlers ([exn:application:match:zero?  
                     (lambda (exn) +inf.0)]) 
      (/ n d)))) 

The following example catches and ignores file exceptions, but lets the enclosing context handle breaks:

(define (file-date-if-there filename) 
  (with-handlers ([not-break-exn? (lambda (exn) #f)]) 
    (file-or-directory-modify-seconds filename))) 

6.1.1  Primitive Exceptions

Whenever a primitive error occurs in MzScheme, an exception is raised. The value that is passed to the current exception handler is always an instance of the exn structure type. Every exn structure value has a message field that is a string, the primitive error message. The default exception handler recognizes exception values with the exn? predicate and passes the error message to the current error display handler (see error-display-handler in section 7.4.1.7).

Primitive errors do not create immediate instances of the exn structure type. Instead, an instance from a hierarchy of subtypes of exn is instantiated. The subtype more precisely identifies the error that occurred and may contain additional information about the error. The table below defines the type hierarchy that is used by primitive errors and matches each subtype with the primitive errors that instantiate it. In the table, each bulleted line is a separate structure type. A type is nested under another when it is a subtype.

For example, applying a procedure to the wrong number of arguments raises an exception as an instance of exn:application:arity. An exception handler can test for this kind of exception using the global exn:application:arity? predicate. Given such an exception, the (incorrect) number of arguments provided is obtained from the exception with exn:application-value, while exn:application:arity-expected accesses the actual arity of the procedure.

  • exn : not instantiated directly

    message field, immutable-string -- error message
    continuation-marks field, mark-set -- value returned by current-continuation-marks immediately after the error is detected
    • exn:user : raised by calling error

    • exn:variable : unbound global or module variable at run time

      id field, symbol -- the unbound variable's global identifier

    • exn:application : not instantiated directly

      value field, value -- the error-specific inappropriate value

    • exn:application:arity : application with the wrong number of arguments

      expected field, arity -- the correct procedure arity as returned by arity

  • exn:application:type : wrong argument type to a procedure, not including divide-by-zero

    expected field, symbol -- name of the expected type

  • exn:application:mismatch : bad argument combination (e.g., out-of-range index for a vector) or platform-specific integer range error

  • exn:application:divide-by-zero : divide by zero; application-value is always zero

  • exn:application:continuation : attempt to cross a continuation boundary or apply another thread's continuation

  • exn:syntax : syntax error, but not a read error

    expr field, syntax object or #f -- illegal expression (or #f if unknown)
    form field, symbol or #f -- the syntactic form name that detected the error (or #f if unknown)
    module field, symbol, module path index, or #f -- the form-defining module (or #f if unknown)

  • exn:read : read parsing error

    source field, value -- source name
    line field, positive exact integer or #f -- source line
    column field, positive exact integer or #f -- source column
    position field, positive exact integer or #f -- source position
    span field, non-negative exact integer or #f -- source span

  • exn:read:eof : unexpected end-of-file

  • exn:read:non-char : unexpected non-character

  • exn:i/o : not instantiated directly

  • exn:i/o:port : not instantiated directly

    port field, port -- port for attempted operation

  • exn:i/o:port:read : error reading from a port

  • exn:i/o:port:write : error writing to a port

  • exn:i/o:port:closed : attempt to operate on a closed port

  • exn:i/o:filesystem : illegal pathname or error manipulating a filesystem object

    pathname field, path -- file or directory pathname
    detail field, symbol or #f -- 'ill-formed-path, 'already-exists, or 'wrong-version, indicating the reason for the exception (if available), or #f

  • exn:i/o:tcp : TCP errors

  • exn:thread : raised by call-with-custodian

  • exn:module : raised by module, require, etc.

  • exn:break : asynchronous thread break

    continuation field, continuation -- a continuation that resumes from the break

  • exn:special-comment : raised by a custom input port's special-reading procedure

    width field, non-negative exact integer -- width of the special comment in port positions

  • exn:misc : low-level or MzScheme-specific error

  • exn:misc:unsupported : unsupported feature

  • exn:misc:out-of-memory : out of memory

Primitive procedures that accept a procedure argument with a particular required arity (e.g., call-with-input-file, call/cc) check the argument's arity immediately, raising exn:application:type if the arity is incorrect.

6.2  Errors

The procedure error raises the exception exn:user (which contains an error string). The error procedure has three forms:

  • (error symbol) creates a message string by concatenating "error: " with the string form of symbol.

  • (error msg-string v ···) creates a message string by concatenating msg-string with string versions of the vs (as produced by the current error value conversion handler; see section 7.4.1.7). A space is inserted before each v.

  • (error src-symbol format-string v ···) creates a message string equivalent to the string created by:

    (format (string-append "~s: " format-string)  
      src-symbol v ···

In all cases, the constructed message string is passed to make-exn:user and the resulting exception is raised.

6.2.1  Application Type Errors

(raise-type-error name-symbol expected-string v) creates an exn:application:type value and raises it as an exception. The name-symbol argument is used as the source procedure's name in the error message. The expected-string argument is used as a description of the expected type, and v is the value received by the procedure that does not have the expected type.

(raise-type-error name-symbol expected-string bad-k v) is similar, except that the bad argument is indicated by an index (from 0), and all of the original arguments v are provided (in order). The resulting error message names the bad argument and also lists the other arguments. If bad-k is not less than the number of vs, the exn:application:mismatch exception is raised.

6.2.2  Application Mismatch Errors

(raise-mismatch-error name-symbol message-string v) creates an exn:application:mismatch value and raises it as an exception. The name-symbol is used as the source procedure's name in the error message. The message-string is the error message. The v argument is the improper argument received by the procedure. The printed form of v is appended to message-string (using the error value conversion handler; see section 7.4.1.7).

6.2.3  Syntax Errors

(raise-syntax-error name message-string [expr sub-expr]) creates an exn:syntax value and raises it as an exception. Macros use this procedure to report syntax errors. The name argument is usually #f when expr is provided; it is described in more detail below. The message-string is used as the main body of the error message. The optional expr argument is the erroneous source syntax object or S-expression. The optional sub-expr argument is a syntax object or S-expression within expr that more precisely locates the error. If sub-expr is provided, it is used (in syntax form) as the expr field of the generated exception record, else the expr is used if provided, otherwise the expr field is #f. Source location information for the error message is similarly extracted from sub-expr or expr, when at least one is a syntax object.

The form name used in the generated error message and the values of the form and module fields of the generated exception are determined through a combination of the name, expr, and sub-expr arguments. The name argument can be any of three kinds of values:

  • #f: When name is #f, and when expr is either an identifier or a syntax pair containing an identifier as its first element, then the form name from the error message is the identifier's symbol, the form field of the exception is the third result of identifier-binding applied to the identifier, and the module field of the exception is the fourth result of identifier-binding applied to the identifier. (See section 12.3.2 for information about identifier-binding.)

    If expr is not provided, or if it is not an identifier or a syntax pair containing and identifier as its first element, then the form name in the error message is "?", the form field of the exception is #f, and the module field of the exception is #f.

  • symbol: When name is a symbol, then the symbol is used as the form name in the generated error message. If expr is provided, and it is either an identifier or a syntax pair whose first element is an identifier, then the exception fields are computed in the same way as when name is #f. Otherwise, the form field of the exception is name, and the module field of the exception is #f.

  • (list msg-symbol form mod): When name is a list of three items, the first is used as the form name in the generated error message, the second (which can be a symbol or #f) is used as the form field of the generated exception, and the last (which can be a module index path, a symbol, or #f) is used as the module field of the generated exception.

See also section 7.4.1.7.

6.2.4  Inferred Value Names

To improve error reporting, names are inferred at compile-time for certain kinds of values, such as procedures. For example, evaluating the following expression:

(let ([f (lambda () 0)]) (f 1 2 3)) 

produces an error message because too many arguments are provided to the procedure. The error message is able to report ``f'' as the name of the procedure. In this case, MzScheme decides, at compile-time, to name as f all procedures created by the let-bound lambda.

Names are inferred whenever possible for procedures. Names closer to an expression take precedence. For example, in

(define my-f 
  (let ([f (lambda () 0)]) f)) 

the procedure bound to my-f will have the inferred name ``f''.

When an 'inferred-name property is attached to a syntax object for an expression (see section 12.6.2), the property value is used for naming the expression, and it overrides any name that was inferred from the expression's context.

When an inferred name is not available, but a source location is available, a name is constructed using the source location information. Inferred and property-assigned names are also available to syntax transformers, via syntax-local-name; see section 12.6 for more information.

(object-name v) returns a symbol or immutable string for the name of v if v has a name, #f otherwise. The argument v can be any value, but only (some) procedures, structs, struct types, struct type properties, regexp values, and input ports have names. Only regexp values and input ports have string names (the source of the regexp, or an absolute path for file input ports); other names are symbols. All primitive procedures have names (see section 3.10.2).

6.3  Continuations

MzScheme supports fully re-entrant call-with-current-continuation (or call/cc). The macro let/cc binds a variable to the continuation in an immediate body of expressions:

 (let/cc k expr ···1=expands=> 
 (call/cc (lambda (k) expr ···1)) 

A continuation can only be invoked from the thread (see Chapter 7) in which it was captured. Multiple return values can be passed to a continuation (see section 2.2).

In addition to regular call/cc, MzScheme provides call-with-escape-continuation (or call/ec) and let/ec. A continuation obtained from call/ec can only be used to escape back to the continuation; i.e., an escape continuation is only valid when the current continuation is an extension of the escape continuation. The application of call/ec's argument is not a tail call.

Escape continuations are provided for two reasons: 1) they are significantly cheaper than full continuations; and 2) full continuations are not allowed to cross certain boundaries (e.g., error handling) that escape continuations can safely cross.

The exn:application:continuation exception is raised when a continuation is applied by the wrong thread, a continuation application would violate a continuation boundary, or an escape continuation is applied outside of its dynamic scope.

6.4  Dynamic Wind

(dynamic-wind pre-thunk value-thunk post-thunk) applies its three thunk arguments in order. The value of a dynamic-wind expression is the value returned by value-thunk. The pre-thunk procedure is invoked before calling value-thunk and post-thunk is invoked after value-thunk returns. The special properties of dynamic-wind are manifest when control jumps into or out of the value-thunk application (either due to an exception or a continuation invocation): every time control jumps into the value-thunk application, pre-thunk is invoked, and every time control jumps out of value-thunk, post-thunk is invoked. (No special handling is performed for jumps into or out of the pre-thunk and post-thunk applications.)

When dynamic-wind calls pre-thunk for normal evaluation of value-thunk, the continuation of the pre-thunk application calls value-thunk (with dynamic-wind's special jump handling) and then post-thunk. Similarly, the continuation of the post-thunk application returns the value of the preceding value-thunk application to the continuation of the entire dynamic-wind application.

When pre-thunk is called due to a continuation jump, the continuation of pre-thunk

  1. jumps to a more deeply nested pre-thunk, if any, or jumps to the destination continuation; then

  2. continues with the context of the pre-thunk's dynamic-wind call.

Normally, the second part of this continuation is never reached, due to a jump in the first part. However, the second part is relevant because it enables jumps to escape continuations that are contained in the context of the dynamic-wind call. Similarly, when post-thunk is called due to a continuation jump, the continuation of post-thunk jumps to a less deeply nested post-thunk, if any, or jumps to a pre-thunk protecting the destination, if any, or jumps to the destination continuation, then continues from the post-thunk's dynamic-wind application.

Example:

(let ([v (let/ec out  
           (dynamic-wind 
            (lambda () (display "in "))  
            (lambda ()  
              (display "pre ")  
              (display (call/cc out)) 
              #f)  
            (lambda () (display "out "))))])   
  (when v (v "post ")))  
 => displays in pre out in post out 

(let/ec k0 
  (let/ec k1 
    (dynamic-wind 
     void 
     (lambda () (k0 'cancel)) 
     (lambda () (k1 'cancel-canceled))))) 
 => 'cancel-canceled 

6.5  Continuation Marks

To evaluate a sub-expression, MzScheme creates a continuation for the sub-expression that extends the current continuation. For example, to evaluate expr1 in the expression

(begin  
  expr1 
  expr2

MzScheme extends the continuation of the begin expression with one continuation frame to create the continuation for expr1. In contrast, expr2 is in tail position for the begin expression, so its continuation is the same as the continuation of the begin expression.

A continuation mark is a keyed mark in a continuation frame. A program can install a mark in the first frame of its current continuation, and it can extract the marks from all of the frames in any continuation. Continuation marks support debuggers and other program-tracing facilities; in particular, continuation frames roughly correspond to stack frames in traditional languages. For example, a debugger can annotate a source program to store continuation marks that relate each expression to its source location; when an exception occurs, the marks are extracted from the current continuation to produce a ``stack trace'' for the exception.

The list of continuation marks for a key k and a continuation C that extends C0 is defined as follows:

  • If C is an empty continuation, then the mark list is null.

  • If C's first frame contains a mark m for k, then the mark list for C is (cons m l0), where l0 is the mark list for k in C0.

  • If C's first frame does not contain a mark keyed by k, then the mark list for C is the mark list for C0.

The with-continuation-mark form installs a mark on the first frame of the current continuation:

(with-continuation-mark key-expr mark-expr  
   body-expr

The key-expr, mark-expr, and body-expr expressions are evaluated in order. After key-expr is evaluated to obtain a key and mark-expr is evaluated to obtain a mark, the key is mapped to the mark in the current continuation's initial frame. If the frame already has a mark for the key, it is replaced. Finally, the body-expr is evaluated; the continuation for evaluating body-expr is the continuation of the with-continuation-mark expression (so the result of the body-expr is the result of the with-continuation-mark expression, and body-expr is in tail position for the with-continuation-mark expression).

The continuation-marks procedure extracts the complete set of continuation marks from a continuation:

  • (continuation-marks cont) returns an opaque value containing the set of continuation marks for all keys in the continuation cont.

  • (current-continuation-marks) returns an opaque value containing the set of continuation marks for all keys in the current continuation. In other words, it produces the same value as (call-with-current-continuation continuation-marks).

The continuation-mark-set->list procedure extracts mark values for a particular key from a continuation mark set:

  • (continuation-mark-set->list mark-set key-v [skip-v]) returns a newly-created list containing the marks for key-v in mark-set, which is a set of marks returned by current-continuation-marks. If skip-v is provided, then it is inserted into the list once for every consecutive sequence of frames without a key-v mark.

  • (continuation-mark-set? v) returns #t if v is a mark set created by continuation-marks or current-continuation-marks, #f otherwise.

Examples:

(define (extract-current-continuation-marks key)  
   (continuation-mark-set->list  
    (current-continuation-marks)  
    key))  
 
(with-continuation-mark 'key 'mark  
  (extract-current-continuation-marks 'key)) ; => '(mark)  
 
(with-continuation-mark 'key1 'mark1  
  (with-continuation-mark 'key2 'mark2  
    (list  
     (extract-current-continuation-marks 'key1)  
     (extract-current-continuation-marks 'key2)))) ; => '((mark1) (mark2))  
 
(with-continuation-mark 'key 'mark1  
  (with-continuation-mark 'key 'mark2 ; replaces the previous mark  
    (extract-current-continuation-marks 'key)))) ; => '(mark2)  
 
(with-continuation-mark 'key 'mark1  
  (list ; continuation extended to evaluate the argument  
   (with-continuation-mark 'key 'mark2  
      (extract-current-continuation-marks 'key)))) ; => '((mark1 mark2))  
 
(let loop ([n 1000]) 
  (if (zero? n)  
      (extract-current-continuation-marks 'key)  
      (with-continuation-mark 'key n 
        (loop (sub1 n))))) ; => '(1) 

In the final example, the continuation mark is set 1000 times, but extract-current-continuation-marks returns only one mark value. Because loop is called tail-recursively, the continuation of each call to loop is always the continuation of the entire expression. Therefore, the with-continuation-mark expression replaces the existing mark each time rather than adding a new one.

Whenever MzScheme creates an exception record, it fills the continuation-marks field with the value of (current-continuation-marks), thus providing a snapshot of the continuation marks at the time of the exception.

When a continuation procedure returned by call-with-current-continuation is invoked, it restores the captured continuation, and also restores the marks in the continuation's frames to the marks that were present when call-with-current-continuation was invoked.

6.6  Breaks

A break is an asynchronous exception, usually triggered through an external source controlled by the user, or through the break-thread procedure (see section 7.1). A break exception can only occur in a thread while breaks are enabled. When a break is detected and enabled, the exn:break exception is raised in the thread sometime afterward; if breaking is disabled when break-thread is called, the break is suspended until breaking is again enabled for the thread. While a thread has a suspended break, additional breaks are ignored.

Breaks are enabled through the break-enabled parameter (see section 7.4.1.8). Certain procedures, such as semaphore-wait/enable-break, enable breaks temporarily while performing a blocking action. However, breaks are always disabled while an exception handler is executing, and cannot be enabled through break-enabled or uses of procedures like semaphore-wait/enable-break. Note that the handling procedures supplied to with-handlers are not exception handlers, so breaking within such procedures is controlled by break-enabled. Breaks are also disabled (independent of break-enabled and .../enable-break) during the evaluation of the ``pre'' and ``post'' thunks for a dynamic-wind, whether called during the normal dynamic-wind calling sequence or via a continuation jump.

If breaks are enabled for a thread, and if a break is triggered for the thread but not yet delivered as an exn:break exception, then the break is guaranteed to be delivered before breaks can be disabled in the thread. The timing of exn:break exceptions is not guaranteed in any other way.

If a break is triggered for a thread that is blocked on a nested thread (see call-in-nested-thread), and if breaks are enabled in the blocked thread, the break is implicitly handled by transferring it to the nested thread.

When breaks are enabled, they can occur at any point within execution, which makes certain implementation tasks subtle. For example, assuming breaks are enabled when the following code is executed,

(with-handlers ([exn:break? (lambda (x) (void))]) 
  (semaphore-wait s)) 

then it is not the case that a void result means the semaphore was decremented or a break was received, exclusively. It is possible that both occur: the break may occur after the semaphore is successfully decremented but before a void result is returned by semaphore-wait. A break exception will never damage a semaphore, or any other built-in construct, but many built-in procedures (including semaphore-wait) contain internal sub-expressions that can be interrupted by a break.

In general, it is impossible using only semaphore-wait to implement the guarantee that either the semaphore is decremented or an exception is raised, but not both. MzScheme therefore supplies semaphore-wait/enable-break (see section 7.2), which does permit the implementation of such an exclusive guarantee:

(parameterize ([break-enabled #f]) 
  (with-handlers ([exn:break? (lambda (x) (void))]) 
    (semaphore-wait/enable-break s))) 

In the above expression, a break can occur at any point until break are disabled, in which case a break exception is propagated to the enclosing exception handler. Otherwise, the break can only occur within semaphore-wait/enable-break, which guarantees that if a break exception is raised, the semaphore will not have been decremented.

To allow similar implementation patterns over blocking port operations, MzScheme provides read-string-avail!/enable-break (see section 11.2.1), write-string-avail/enable-break (see section 11.2.2), and other procedures.

6.7  Error Escape Handler

Special control flow for exceptions is performed by an error escape handler that is called by the default exception handler. An error escape handler takes no arguments and must escape from the expression that raised the exception. The error escape handler is obtained or set using the error-escape-handler parameter (see section 7.4.1.7).

An error escape handler cannot invoke a full continuation that was created prior to the exception, but it can invoke an escape continuation (see section 6.3).

The error escape handler is normally called directly by an exception handler. To escape from a run-time error, use raise (see section 6.1) or error (see section 6.2) instead.

If an exception is raised while the error escape handler is executing, an error message is printed using a primitive error printer and a primitive error escape handler is invoked.

In the following example, the error escape handler is set so that errors do not escape from a custom read-eval-print loop:

(let ([orig (error-escape-handler)]) 
  (let/ec exit 
    (let retry-loop () 
      (let/ec escape 
        (error-escape-handler 
         (lambda () (escape #f))) 
        (let loop () 
          (let ([e (my-read)]) 
            (if (eof-object? e) 
                (exit 'done) 
                (let ([v (my-eval e)]) 
                  (my-print v) 
                  (loop)))))) 
      (retry-loop))) 
  (error-escape-handler orig)) 

See also read-eval-print-loop in section 14.1 for a simpler implementation of this example.