Error handling
try/catch/finally, throw, panic, and defer for recoverable and unrecoverable errors.
Summary
Any value can be thrown: strings, ints, maps, whatever makes sense. finally always runs, even when an exception is thrown. panic(msg) terminates immediately and is not catchable. defer schedules a block to run on function return, in LIFO order, even through exceptions. todo() and unreachable() both panic and are used as code-structure markers.
Canonical
Try / Catch / Finally
try {
throw "something went wrong"
} catch e {
println("Error: {e}") -- Error: something went wrong
}
-- throw any value (string, int, map, whatever)
try {
throw #{"kind": "NotFound", "msg": "missing"}
} catch e {
println(e["msg"]) -- missing
}
-- finally always runs
try {
throw "err"
} catch e {
println("caught")
} finally {
println("cleanup") -- always executes
}Nested Try/Catch
try {
try {
throw "inner"
} catch e {
throw "rethrown: {e}"
}
} catch e {
println(e) -- rethrown: inner
}Throw from Functions
Exceptions propagate up the call stack:
fn divide(a, b) {
if b == 0 { throw "division by zero" }
return a / b
}
try {
divide(10, 0)
} catch e {
println(e) -- division by zero
}Panic
panic(msg) terminates immediately. It is not catchable by try/catch.
panic("fatal: out of memory")
-- prints to stderr: xs: panic: fatal: out of memory
-- exits with code 1Defer
defer schedules a block to run when the enclosing function returns. Multiple defers execute in LIFO (last-in, first-out) order.
fn example() {
defer { println("third") }
defer { println("second") }
defer { println("first") }
println("body")
}
example()
-- body
-- first
-- second
-- thirdDefers run even if an exception is thrown.
When to Use What
| Mechanism | Catchable? | Use case |
|---|---|---|
throw expr | Yes | Recoverable errors: bad input, validation, missing data |
panic(msg) | No | Unrecoverable: invariant violations, impossible states |
todo(msg?) | No | Placeholder for unimplemented code |
unreachable() | No | Code that should never execute |