This guide covers the Turmeric error handling story: Result, Option, panic,
unwrap helpers, and contract macros. It describes what is implemented today.
Turmeric offers two primary error handling strategies:
Result -- recoverable errors; use when callers should handle failure.panic -- unrecoverable errors; use for programming mistakes and invariant violations.Option is a separate type for nullable / optional values and has its own unwrap helpers.
Contract macros (assert!, require!, ensure!, invariant!) provide structured
precondition and postcondition checking built on top of panic.
ResultA result value is either (ok value) or (err error). The runtime representation
is a heap-allocated struct { bool is_ok; int64_t ok_val; int64_t err_val; } returned
as ptr<void>. Both the ok and err fields are int64_t in v1; typed generics are a
future follow-on.
(ok 42) ;; ok result
(err 99) ;; err result
(ok? r) ;; => bool
(err? r) ;; => bool
These are unsafe -- check ok? / err? first:
(ok-val r) ;; => int (undefined behaviour if r is err)
(err-val r) ;; => int (undefined behaviour if r is ok)
(result-unwrap r) ;; ok value, or abort() with message to stderr
(result-unwrap-or r default) ;; ok value, or default
(result-expect r "message") ;; ok value, or abort() printing "message"
(result-must r) ;; ok value, or panic "result-must: called on err"
(result-must-msg r "msg") ;; ok value, or panic with "msg"
Note:
result-unwrapandresult-expectcallabort()directly (not viatur_panic). Preferresult-must/result-must-msgwhen you want the standard panic message format and double-panic guard.
(result-map r f) ;; apply f to ok value; propagate err unchanged
(result-map-err r f) ;; apply f to err value; propagate ok unchanged
(result-flat-map r f) ;; f receives ok value and returns a new result
(result-or r alt) ;; r if ok, else alt
(result-or-else r f) ;; r if ok, else (f err-value)
(result-eq? r1 r2 ok-cmp err-cmp)
;; ok-cmp -- fn [a b :int] :bool used when both are ok
;; err-cmp -- fn [a b :int] :bool used when both are err
;; => bool; false if variants differ
The Eq typeclass instance uses = for both sides:
(eq? (ok 1) (ok 1)) ;; => true
(eq? (ok 1) (err 1)) ;; => false
(ok-or opt err-val) ;; some(v) -> ok(v), none -> err(err-val)
(err-context r "prefix") ;; prepend "prefix: " to the err string; ok passes through
(result-collect vec) ;; (vec result) -> result<vec, E>; first err wins
(result-partition vec) ;; -> pair; separate ok and err elements
(result-partition-ok pair) ;; -> vec of ok values from the pair
(result-partition-err pair) ;; -> vec of err values from the pair
(result-free r) ;; free the heap struct (does not free the contained value)
result (as ptr<void>) implements Functor, Applicative, Monad,
Foldable, Bifunctor, and Eq. These are defined in stdlib/result.tur.
| Typeclass | Behaviour on ok / err |
|---|---|
Functor |
fmap applies to ok value; err propagates |
Applicative |
pure x = ok(x); ap short-circuits on first err |
Monad |
bind applies f to ok value; err propagates |
Foldable |
foldl / foldr operate on ok value; err is skipped |
Bifunctor |
bimap fn-left fn-right -- fn-left over err, fn-right over ok |
Eq |
structural equality via result-eq? with = comparator |
OptionAn option is either (some value) or (none). none is represented as NULL.
(some 42) ;; some option
(none) ;; none option (NULL)
(some? o) ;; => bool
(option-unwrap o) ;; value, or exit(1) with message to stderr
(option-must o) ;; value, or panic "option-must: called on none"
(option-expect o "message") ;; value, or panic with "message"
Note:
option-unwrapcallsexit(1)directly. Preferoption-must/option-expectfor consistent panic semantics.
(ok-or opt err-val) ;; some(v) -> ok(v), none -> err(err-val)
(option-eq? o1 o2 cmp-fn)
;; cmp-fn -- fn [a b :int] :bool
;; => true if both none, or both some with cmp-fn returning true
The Eq typeclass instance uses = for the contained value.
(option-free o) ;; free the heap struct
option implements Functor, Applicative, Monad, Foldable, Traversable,
Alternative, Eq, Clone, and Show. These are defined in stdlib/option.tur.
panicpanic terminates the program unconditionally. Use it for:
(panic "something went wrong")
This prints panic: something went wrong to stderr and calls abort().
If tur_panic is called while a panic is already in progress (e.g. in a defer
chain), it prints double panic: aborting and calls abort() immediately.
defer during panicDefer thunks registered before the panic are fired in reverse order during
unwinding. If a defer thunk itself panics, the double-panic guard triggers abort().
must! and must-msg!These macros unwrap an option value, panicking on none:
(must! (some 42)) ;; => 42
(must! (none)) ;; panic: option-must: called on none
(must-msg! (some 42) "expected value") ;; => 42
(must-msg! (none) "expected value") ;; panic: expected value
must!expands to(option-must expr).must-msg!expands to(option-expect expr msg). Forresultvalues useresult-must/result-must-msgdirectly -- the macros do not dispatch generically.
option-must and option-expectThese are the underlying functions used by must! and must-msg!:
(option-must (some 42)) ;; => 42
(option-must (none)) ;; panic: option-must: called on none
(option-expect (some 42) "want value") ;; => 42
(option-expect (none) "want value") ;; panic: want value
ignore!Explicitly discard a result or option value to silence unused-value warnings (when linting is enabled):
(ignore! (some-fn-returning-result))
ignore! expands to (do expr nil) -- the expression is evaluated for its side
effects and the value is dropped.
Contract macros live in stdlib/macros.tur (module tur/macros) and are
auto-imported. They all expand to tur-contract-check or tur-contract-check-inv
from stdlib/contract.tur, which call tur_panic on failure.
In v1, contracts are always enabled (contract-enabled? returns true).
A --no-contracts flag is planned for Phase C2.
assert! and assert-msg!Unconditional sanity check. Use anywhere you want to verify an intermediate condition holds:
(assert! (= x 1))
;; panics with "Assertion failed" if x != 1
(assert-msg! (= x 1) "x must be 1")
;; panics with "x must be 1" if x != 1
require! and require-msg!Precondition check at function entry:
(defn sqrt [n :int] :int
(require! (>= n 0))
...)
;; panics with "Precondition failed" if n < 0
(defn sqrt [n :int] :int
(require-msg! (>= n 0) "sqrt: n must be non-negative")
...)
ensure! and ensure-msg!Postcondition check before returning from a function:
(defn abs [n :int] :int
(let [result (if (< n 0) (- 0 n) n)]
(ensure! (>= result 0))
result))
;; panics with "Postcondition failed" if the result is somehow negative
invariant! and invariant-msg!Structural invariant check -- passes the value to a predicate function:
(invariant! my-list non-empty?)
;; panics with "Invariant failed" if (non-empty? my-list) is false
(invariant-msg! my-list non-empty? "list must not be empty")
| Situation | Approach |
|---|---|
| Caller should handle the failure (e.g. file not found) | result |
| Value may or may not be present | option / some / none |
| Programming error / violated invariant | panic |
| Unwrapping a result you are confident is ok | result-must / result-must-msg |
| Unwrapping an option you are confident is some | option-must / must! |
| Checking a precondition at function entry | require! / require-msg! |
| Checking a postcondition before returning | ensure! / ensure-msg! |
| Verifying a structural invariant | invariant! / invariant-msg! |
| General sanity check in the middle of code | assert! / assert-msg! |
| Discarding a result intentionally | ignore! |
The following features are planned but not yet implemented:
| Feature | Phase | Notes |
|---|---|---|
? operator |
R1 | Short-circuit error propagation in the caller |
catch-unwind |
R2 | Catch a panic at a safe boundary |
--no-contracts flag |
C2 | Strip contracts from release builds |
--warn-unused-result compiler flag |
R6 | Lint for dropped results |
--lint-panic compiler flag |
R6 | Audit panic call sites |
When a panic occurs inside an effect handler or continuation:
catch-unwind boundary (Phase R2) is present.reset boundary. Resuming
a continuation (shift/reset) after a panic clears the panic state; it does
not re-panic automatically.abort().In v1, (async fn) calls the function synchronously with no fiber scheduler. A
panic inside an async body propagates through the call stack and is not caught at
any task boundary.
The v2 target behavior (when true fiber-based async lands):
catch-unwind at
the join point to recover.unreachable instruction.