Error Handling
Lattice provides three complementary mechanisms for error handling: exception-based
try/catch, guaranteed cleanup with defer, and
value-based result propagation with the ? operator.
Try/Catch
try_catch ::= "try" block "catch" identifier block
A try/catch expression executes the try block. If a runtime
error occurs, execution transfers to the catch
block with the error value bound to the identifier.
The try/catch expression produces a value: the value of the try block on success, or the value of the catch block on error.
let result = try { let data = json_parse("invalid json") data.get("name") } catch e { print("Error: ${e}") "default" }
Runtime Errors
Runtime errors are raised by invalid operations (e.g., type mismatches, index out of bounds,
crystal assignment) and by failed require/ensure contracts.
These errors propagate up the call stack until caught by a try/catch
block. If uncaught, the program terminates with an error message.
The error() built-in creates an error result map (with "tag": "err"
and "value" fields), which can be used with the ? operator for
value-based error handling. The is_error() built-in checks whether a value
is an error map.
fn divide(a: Int, b: Int) -> Map { if b == 0 { return error("division by zero") } return ok(a / b) }
Defer
defer_stmt ::= "defer" block
A defer statement schedules a block to be executed when the enclosing scope
exits. Deferred blocks run regardless of whether the exit is normal or due to an error.
This makes defer ideal for cleanup operations like closing file handles or releasing resources.
Multiple defers in the same scope execute in LIFO (last-in, first-out) order: the most recently deferred block runs first.
fn with_file(path: String) { let fd = open(path) defer { close(fd) } // 3rd: close file defer { print("done") } // 2nd: log defer { print("cleaning up") } // 1st: log // ... work with fd ... // All defers run when this scope exits }
Result Type Pattern
Lattice uses a map-based convention for result values. The built-in error(value)
creates a map with "tag": "err" and "value" fields. The built-in
is_error(value) checks for this tag.
The standard library (lib/fn.lat) provides additional helpers:
ok(value), err(value), is_ok(result), and
is_err(result). These must be imported before use.
fn parse_int(s: String) -> Map { let n = to_int(s) if n == nil { return error("not a number: ${s}") } // Return a success map flux result = Map::new() result.set("tag", "ok") result.set("value", n) return result } // error(value) creates {"tag": "err", "value": value} // is_error(value) checks if value has tag "err"
Try-Propagate Operator (?)
try_propagate ::= expression "?"
The postfix ? operator works with result maps. If the expression evaluates
to a map containing an "err" key, the error is immediately returned from the
enclosing function as an error result. Otherwise, the "ok" value is extracted
and becomes the expression's value.
fn load_config() -> Map { let raw = read_file("config.json")? // unwrap or return err let parsed = json_parse(raw)? // unwrap or return err return ok(parsed) }
This provides ergonomic error propagation without explicit branching.
Optional Chaining
Optional chaining operators provide safe navigation through potentially nil values:
| Operator | Description | Equivalent |
|---|---|---|
x?.field | Access field if x is not nil | if x != nil { x.field } else { nil } |
x?.method() | Call method if x is not nil | if x != nil { x.method() } else { nil } |
x?[i] | Index if x is not nil | if x != nil { x[i] } else { nil } |
Optional chaining composes with the nil-coalesce operator ?? to provide
defaults:
let city = user?.address?.city ?? "Unknown" let first = items?[0]?.name ?? "none"
Lattice