Zith uses fully static, return-based error handling. No exceptions, no hidden control flow. No semicolon required after ? or !.
| Syntax | Meaning | Propagated by |
|---|---|---|
?T |
Optional — T or null. | ? (postfix) |
T! |
Result — T or an error. Equivalent to Rust's Result<T, E> where E must implement the Error trait. |
! (postfix) |
Failable types may be stacked — notation is linear and unambiguous:
// Optional pointer to an optional Result of optional i32 or IoError
?*?(?i32 ! IoError)
must vs raw| Debug mode | Release mode | |
|---|---|---|
must |
Panics with file + line location. | Compiler guides you to remove, becomes an if/else + early return with a custom error code. |
raw |
Always unchecked. | Always unchecked. |
let cfg: ?Config = tryLoad();
let c1 = must cfg; // panics debug; compiler warns/guides in release
let c2 = raw cfg; // always unchecked; compiler always warns
fn readConfig(path: string): Config! {
let file = File.open(path)!
let data = file.read()!
parse(data)!
}
let name = ?user.name or "guest";
let data = !primary() or backup() or default;
// Propagation inside chain flow
readFile("data.bin") -> parse(..)! -> validate(..)? -> process(..)
? / !, raw, must are needed to access failable types:
or alternative. The or value becomes the Integral — the valid, unwrapped T.or alternative. The or value becomes the Integral.ℹ Integral = the valid, non-null, non-error value of a failable type — the inner T that ? and ! unwrap to.
with / catch// Short-circuit: first failure jumps to catch
with (connectDb(), user: getUser(id)) {
process(user);
} catch (err) { log(err); }
// Eager: all expressions evaluated; catch runs if any failed
with! (a: fetchA(), b: fetchB()) { use(b); }
catch { log(a, b); }
fail BlocksA fail block runs when an error would escape the associated scope. Can appear after a named block (external) or inside a block (nameless scope guard):
// External fail
loadConfigure {
let raw = readFile("config.json")!
parse(raw)!
} fail loadConfigure(err) {
if (err is NotFound) { continue(default); }
throw Error{ context: "load failed", cause: err };
}
// Nameless fail — guards current scope
{
fail (err) { log("scope error:", err); }
risky()!
another()!
}
Options inside a fail block:
continue(value) — resume after the block with a replacement value.return value; — exit the enclosing function.throw value; — propagate a new error (requires Error capability).throwfn divide(a: i32, b: i32): i32! {
if (b == 0) throw DivisionByZero;
a / b
}
See also: Spec: Error Handling
Next: Generics