Error Handling

← Back to Language Guide

Eta’s exception model is built on two special forms: raise and catch. They compile directly to the Throw and SetupCatch opcodes on the VM and integrate cleanly with dynamic-wind and the logic-trail unwinder.


Forms

FormMeaning
(raise tag value)Unwind the stack to the nearest matching catch.
(catch tag body …)Evaluate body. If a raise with the same tag escapes, return its payload.
(catch body …)Catch-all. Intercepts any raise and any runtime.* error.
(dynamic-wind before body after)Run before, then body, then afterafter runs even on exception unwind.

Tags are ordinary symbols. The raised value can be any Eta value — number, string, list, record.


Basics

(catch 'err 42)                            ; => 42  (no raise)
(catch 'err (raise 'err "oops"))           ; => "oops"
(catch 'math (raise 'math 404))            ; => 404
(catch       (raise 'anything "caught"))   ; => "caught"   ; catch-all

Runtime errors

Built-in failures are tagged in the runtime.* namespace:

TagTrigger
runtime.type-errorWrong argument type
runtime.invalid-arityWrong number of arguments
runtime.unboundReference to an unbound name
runtime.div-by-zeroInteger division by zero
runtime.index-out-of-rangeVector / string index out of bounds
runtime.errorAny runtime error (parent tag)
(catch 'runtime.type-error (car 42))
;; => (runtime-error runtime.type-error "car: argument must be a pair" <span> <stack>)

(catch (car 42))                ; tag-less catch-all also intercepts it

The payload of a runtime error is a tuple:

(runtime-error <tag> <message-string> <span-record> <stack-trace>)

<span-record> ::= (span <file-id> <start-line> <start-col> <end-line> <end-col>)
<stack-trace> ::= ((frame <function-name> <span-record>))

Tag specificity

Handlers are matched inside out. Inner handlers fire first when their tag matches; otherwise the raise propagates outward.

(catch 'outer
  (+ 10 (catch 'inner
          (raise 'inner 5))))           ; => 15

(catch
  (catch 'wrong-tag
    (raise 'real-tag "bypassed")))      ; => "bypassed"

Structured payloads

Use any data shape; pairs of (code . detail) are a common idiom:

(defun validate-age (n)
  (cond
    ((< n 0)   (raise 'validation (cons 'negative n)))
    ((> n 150) (raise 'validation (cons 'too-large n)))
    (else      n)))

(catch 'validation (validate-age -3))   ; => (negative . -3)

Early exit

raise is a fast non-local exit — useful for short-circuiting deep recursion:

(defun first-negative (xs)
  (catch 'found
    (let loop ((xs xs))
      (cond
        ((null? xs)     #f)
        ((< (car xs) 0) (raise 'found (car xs)))
        (else           (loop (cdr xs)))))))

(first-negative '(3 1 -4 2))            ; => -4

Resource cleanup

dynamic-wind guarantees the after thunk runs even when an exception escapes the body:

(define cleanup-ran #f)

(catch 'boom
  (dynamic-wind
    (lambda () (set! cleanup-ran #f))
    (lambda () (raise 'boom "ow"))
    (lambda () (set! cleanup-ran #t))))

cleanup-ran                              ; => #t

Re-raising

An inner handler can wrap and re-raise:

(defun wrap (thunk)
  (catch 'low
    (let ((v (thunk)))
      ;; v holds the intercepted low-level payload; wrap it
      (raise 'high (list 'wrapped v)))))

Implementation notes

See Bytecode VM for the opcodes.