# Error Handling
Deft errors are values — specifically tagged `%Err{ :message ... }` maps. This
page covers `try`/`catch`, `throw`, the Result type (`%Ok`/`%Err`), and the
combinators + propagation forms for chaining Result-returning code.
> **Design note:** Ok/Err are ordinary tagged maps. They flow through `set`,
> function args, and pipelines *unchanged* — no auto-unwrap, no implicit
> short-circuiting. The caller always decides what to do with a Result.
> Explicit tools (`err!`, `and-then`, `unwrap`, `match`) do the unwrapping.
## Three Tools
| Tool | Use when |
|---|---|
| `try` / `catch` | You can recover from the failure locally. |
| Result (`%Ok` / `%Err`) | The failure is a value — the caller decides what to do. |
| `throw` | A genuinely exceptional condition the caller can't be expected to handle. |
## `try` / `catch`
`try { body } catch name { handler }` catches any thrown error and binds it to
`name`:
def safe-read {|path^string| try { set raw [fs/read $path] [json/parse $raw] } catch e { echo "failed: $e~message" %{} } }
The caught value is a `%Err{ :message ... }` map. Access its fields with `~`.
### Multiple statements in a block
Both `try` and `catch` blocks may contain any number of statements; the value
of the last statement is the block's value:
try { set raw [fs/read $path] set parsed [json/parse $raw] parsed~users } catch e { log/error "parse failed" $e @{} }
### Re-throwing
try { [dangerous-op] } catch e { if ($e~message == "transient") { [retry] # swallow and retry } else { [throw $e] # re-throw } }
## `throw`
`throw` raises an `%Err`. Inside a `try`, it's caught; outside, it aborts the
script (or the panel's app thread, in a TUI host).
def divide {|a b| if ($b == 0) { [throw "division by zero"] } ($a / $b) }
You can throw a string (becomes the `:message` of an `%Err`) or a full `%Err`
map with extra fields:
[throw %Err{ :message "invalid input" :code 422 :field "email" }]
## Result Type
For predictable, recoverable failures, prefer the Result type over exceptions. A
Result is either `%Ok{ :value ... }` or `%Err{ :message ... }`. These are
ordinary tagged maps — they flow through `set`, function args, and pipelines
unchanged.
### Constructing Results
%Ok{ value 42 } # literal tagged-map construction
%Err{ message "nope" }
[ok 42] # constructor functions (equivalent)
[err "nope"]
`[ok]` and `[err]` are convenient when wrapping a computed value:
def safe-parse {|raw^string| if [empty? $raw] { %Err{ message "empty input" } } else { try { %Ok{ value [json/parse $raw] } } catch e { $e # already an %Err } } }
### Predicates
[ok? $r] # true if %Ok [err? $r] # true if %Err
### Unwrapping
`unwrap` extracts the value of an `%Ok`, or returns a default for `%Err`:
set v [unwrap $r 0] # the value if Ok, 0 if Err set v [unwrap $r] # the value if Ok, the message if Err
For explicit branch handling, use `match`:
match $r { %Ok{ value $v } => [process $v] %Err{ message $m} => [log/error $m] }
## Marking Result-returning Functions
### `def name^Result` (checked)
An explicit return-type annotation. The compiler records the contract on the
closure; at runtime, every return path is checked — if the returned value isn't
`%Ok` or `%Err`, a warning is logged to stderr.
def load-config^Result {|path^string| if [nil? $path] { return [err "no path"] } %Ok{ value $path } } def buggy^Result {|x| if ($x == 0) { return "zero" } # ⚠ warns: ^Result fn returned string %Ok{ value $x } }
The check is runtime (not static) — it fires when execution actually reaches the
offending return. Cheap on un-annotated functions (one branch per return to skip
the check). Currently `^Result` is the only enforced return type; other
annotations (`^string`, `^number`) are documentation only.
## Chaining Result-returning Operations
Two complementary styles: `ok!` for imperative sequences, `result/*` combinators
for functional pipelines.
### The `ok!` and `err!` assert functions
Two paired stdlib functions that *assert* a Result's variant and either extract the
payload or early-return from the enclosing function:
| Form | `%Ok` | `%Err` | non-Result |
|---|---|---|---|
| `[ok! expr]` | unwraps to `:value` | early-returns the Err | passthrough |
| `[err! expr]` | early-returns the Ok | extracts `:message` | passthrough |
`ok!` is the common form for forward-progress code — "I expect this to succeed;
give me the value, or bail." `err!` is its symmetric counterpart for
error-handling paths — "I expect this to have failed; give me the message."
Each has two equivalent syntactic shapes:
[ok! [fs/read $path]] # bracket form [fs/read $path](ok!) # postfix form — same semantics [err! $r] # bracket form [$r](err!) # postfix form
The postfix form reads naturally as "call ok! on the result" — visible at the
call site, no tiny glyph to miss at end-of-line.
def load-config^Result {|path^string| set raw [ok! [fs/read $path]] # unwrap Ok, or propagate Err set parsed [ok! [json/parse $raw]] %Ok{ value $parsed } } def report-failure^Result {|r| set msg [err! $r] # extract Err message, or propagate Ok [log "got error: $msg"] }
Both work on variables directly — no wrapping required:
def process^Result {|r| set v [ok! $r] # unwraps Ok, propagates Err ($v + 1) }
`ok!` and `err!` are compiler intrinsics, not regular functions — they can't be
redefined. The early-return semantics require compiler support (a runtime
function can't exit its caller). For pure unwrap without early-exit, use
`[unwrap $r]` or `[unwrap $r default]` instead.
### Stdlib combinators (functional style)
| Form | Purpose |
|---|---|
| `[result/map $r $f]` | Lift a pure function into Result: `Ok(v)` → `Ok(f(v))`, Err propagates |
| `[and-then $r $f]` | Monadic bind (and-then): `Ok(v)` → `f(v)`, Err propagates. `f` must return a Result |
| `[result/traverse $coll $f]` | Map `f` over a collection, short-circuit on first Err. Returns `Ok([values])` |
| `[result/sequence $coll-of-results]` | Turn `[Result a]` into `Result [a]`, short-circuit on first Err |
# bind: chain Result-returning steps set r [and-then [op-a $x] {|a| [and-then [op-b $a] {|b| [ok ($a + $b)]}]}] # traverse: validate a list, bail on first failure set r [result/traverse $inputs {|x| [validate $x]}] match $r { %Ok{ value $validated } => [process-all $validated] %Err{ message $m } => [log/error $m] }
`ok!` is usually cleaner for imperative sequences; `and-then` and
`result/traverse` are usually cleaner for functional pipelines.
## Which Should I Use?
### Recover locally → `try` / `catch`
def parse-or-default {|raw| try { [json/parse $raw] } catch _e { %{} } }
### Pass the decision up → Result
def fetch-user^Result {|id^string| # caller decides how to handle %Ok vs %Err [http/get "$api/users/$id"] }
### Bail on first failure → `ok!`
def load-config^Result {|path^string| set raw [ok! [fs/read $path]] set parsed [ok! [json/parse $raw]] %Ok{ value $parsed } }
### Validate a collection → `result/traverse`
def validate-all^Result {|inputs| [result/traverse $inputs {|x| [validate $x]}] }
### Genuinely exceptional → `throw`
def assert-positive {|n^number| if ($n < 0) { [throw "expected positive, got $n"] } $n }
## Conventions
- Most stdlib calls that can fail return a Result. Inspect with `err?`, `unwrap`,
`match`, or `err!`.
- Throw on invariant violations (programmer error); return `%Err` on expected
failures (user input, network, etc.).
- Annotate Result-returning functions with `^Result` so the runtime checker can
catch missing wraps.
- Include a `_` branch in `match` against `%Err` if you want to handle multiple
error kinds — `match` throws on no-match.
- `try` blocks compose cleanly with `defer` — deferred cleanups run even when an
exception is thrown.
## Quick Reference
| Form | Purpose |
|---|---|
| **Construction** | |
| `[ok value]` / `%Ok{ value ... }` | Construct an Ok result |
| `[err message]` / `%Err{ message ... }` | Construct an Err result |
| **Inspection** | |
| `[ok? x]`, `[err? x]` | Result predicates |
| `[unwrap result ?default]` | Get the inner value (no early-exit) |
| `match $r { %Ok{ value $v } => ... %Err{ message $m } => ... }` | Destructure Result |
| **Marking** | |
| `def name! {|args| ...}` | Informal Result/mutation marker (documentation only) |
| `def name^Result {|args| ...}` | Checked Result-returning contract (warns on violation) |
| **Propagation** | |
| `[ok! expr]` or `[expr](ok!)` | Assert Ok: unwrap `:value` or early-return the Err |
| `[err! expr]` or `[expr](err!)` | Assert Err: extract `:message` or early-return the Ok |
| `[result/map $r $f]` | Lift pure `f` into Result |
| `[and-then $r $f]` | Monadic bind (and-then) |
| `[result/traverse $coll $f]` | Map+short-circuit over a collection |
| `[result/sequence $coll]` | `[Result]` → `Result [...]` |
| **Exceptions** | |
| `[throw value]` | Raise an error |
| `try { ... } catch e { ... }` | Catch any thrown error |