(rontolisp) docs
← Macros

handler-case

(handler-case expression (type ([var]) body...)... [(:no-error ([var]) body...)])

Evaluates expression; when an error is signaled during it, control transfers to the first clause whose condition type matches the signaled condition, with var (optional) bound to the condition object, and the clause body's value becomes the value of the whole form. When no clause matches, the error propagates outward (an enclosing handler-case may still catch it). The clause type is any typecase specifier, including condition classes defined by define-condition and the built-in hierarchy (condition > serious-condition > error, warning); an error signaled without a condition object is caught as the class its cause names, with the message in the condition's format-control slot (as the control that prints it verbatim, every ~ doubled): a plain (error "...") is a simple-error, while a failure inside a built-in carries its own type -- a bad car, a wrong argument type or an out-of-range index is a type-error, a zero divisor a division-by-zero, a call to an undefined function an undefined-function, a read of an unbound variable an unbound-variable (on the wasm-GC backends the reachable cases are the undefined-function one, caught as an undefined-function naming the function in a program that names that class and as a simple-error otherwise, a wrong-type argument, a type-error there too, a zero divisor, a division-by-zero there too, and an unbound name read by symbol-value, a reference to a special variable declared without a value or one to a global before its first assignment, an unbound-variable naming the variable there too -- see below), and a call whose keyword tail is malformed -- a keyword the operator does not accept, an odd tail, a non-keyword in keyword position -- a program-error on every backend (unless :allow-other-keys t admits the extra keys; the compiled backends also warn at compile time, since the call is known to fail), and a parse-integer over a string that is no integer a parse-error on every backend. The :no-error clause runs on normal completion with var bound to the (primary) value, outside the handler. Non-local exits (return/return-from) pass through uncaught, and an unwind-protect inside the expression runs its cleanup before the handler.

handler-case is supported on every backend except --no-gc (a compile error there). On the wasm-GC backends (Preview 1 and --component, including wasmtime serve) it compiles through the WebAssembly exception-handling proposal. A program without catching forms is byte-identical to before and keeps its usual command line. Divergence: the WASM backends catch signaled conditions only — a runtime trap stays uncatchable there. A wrong-type argument reaching an arithmetic or comparison operator, car/cdr, an array or nthcdr index, numerator/denominator, random or complex ((+ 1 nil), (< 1 "x"), (car 5), (aref v nil)) is signaled and caught on every backend, with the same message naming the operator and the type it requires — (+ 1 nil) reports +: The value NIL is not of type NUMBER, (car 5) reports CAR: The value 5 is not of type LIST — as a type-error answering type-error-datum and type-error-expected-type. So is an array subscript outside its dimension: (aref (vector 1 2 3) 5) reports AREF: The value 5 is not of type (INTEGER 0 (3)) everywhere, its expected type the list (INTEGER 0 (3)). So is a subseq range outside its sequence: (subseq "abc" 2 1) reports SUBSEQ: invalid bounds 2, 1 for string of length 3 everywhere, its datum the first bound outside its range (1) and its expected type that range, (INTEGER 2 3). So is a sequence operator, an array accessor or a hash-table accessor handed a value of the wrong kind: (find 1 5) reports FIND: The value 5 is not of type SEQUENCE, (aref 5 0) AREF: The value 5 is not of type ARRAY, (gethash 1 5) GETHASH: The value 5 is not of type HASH-TABLE. So is a division by an exact zero -- /, the two-argument floor/ceiling/truncate/round family, mod, rem, at any integer size or over a ratio: (/ 1 0) and (mod 7 0) report Division by zero everywhere, as a division-by-zero (an arithmetic-error). The rounding family and mod/rem signal it over a finite float too, by an exact or a float zero ((floor 7.5 0.0), (mod 7.5 0)); / and expt with a float operand stay IEEE ((/ 1.5 0) is an infinity). So is a left ash whose result cannot be built: (ash 1 (expt 2 70)) reports ASH: shift count too large: 1180591620717411303424 everywhere, as a simple-error. Handlers are per thread of control, so concurrent rontolisp:http-handler requests do not interfere. To run a handler at the signal point without unwinding — e.g. to invoke a restart-case restart — use handler-bind.

Typed conditions dispatch through the class hierarchy, first matching clause wins:

An error a built-in raises dispatches on its class -- what a test framework's (signals form 'type-error) asserts. On the wasm-GC backends this particular failure traps instead (see the divergence above), so it is caught on the interpreter and the JVM:

A malformed call is a program-error everywhere, with the same message -- the test suite shape (signals-error (remove 'a nil :bogus t) program-error):