Error Handling

Zith uses fully static, return-based error handling. No exceptions, no hidden control flow. No semicolon required after ? or !.

Failable Types

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

Propagation & Fallback

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:

ℹ 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 Blocks

A 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:

throw

fn divide(a: i32, b: i32): i32! {
    if (b == 0) throw DivisionByZero;
    a / b
}

See also: Spec: Error Handling


Next: Generics