Error Handling

Zolo has multiple layers of error handling: the Result type for recoverable errors, the ? propagation operator, and try/catch/finally for exception-style handling.

Result Type (Recap) #

The idiomatic way to handle errors is with Result<T, E>:

fn divide(a: int, b: int) -> Result<int, str> {
    if b == 0 {
        return Result.Err("division by zero")
    }
    return Result.Ok(a / b)
}

match divide(10, 2) {
    Result.Ok(v)  => print("Result: {v}"),
    Result.Err(e) => print("Error: {e}"),
}

The ? Operator

The ? operator propagates errors automatically. If the value is Err, the function returns early with that error; if it is Ok, it unwraps the value:

fn parse_int(s: str) -> Result<int, str> {
    // ... returns Ok or Err
}

fn compute(a: str, b: str) -> Result<int, str> {
    let x = parse_int(a)?   // returns Err early if parse fails
    let y = parse_int(b)?
    return Result.Ok(x + y)
}

print(compute("10", "20"))   // Result.Ok(30)
print(compute("10", "oops")) // Result.Err("invalid integer: oops")

Try / Catch / Finally #

For exception-style error handling, Zolo provides try/catch/finally:

try {
    let data = read_file("config.json")
    process(data)
} catch e {
    print("Error: {e}")
}

With finally

The finally block always runs, whether or not an error occurred:

let file = open("data.txt")

try {
    let content = file.read()
    process(content)
} catch e {
    print("Read error: {e}")
} finally {
    file.close()   // always executed
}

Try as Expression #

try/catch can be used as an expression that returns a value:

let result = try {
    parse_json(raw_input)
} catch _ {
    default_config()
}

Nested Try/Catch #

try {
    let user = try {
        fetch_user(id)
    } catch _ {
        create_guest_user()
    }

    let data = fetch_data(user.id)
    render(data)
} catch e {
    show_error_page(e)
}

Panic #

panic terminates the program with an error message. Use it for unrecoverable situations:

fn get_index(arr: [int], i: int) -> int {
    if i < 0 or i >= arr.len() {
        panic("index {i} out of bounds for array of length {arr.len()}")
    }
    return arr[i]
}

Guard Clauses #

guard is an early-return idiom for preconditions (similar to Swift's guard):

fn process(input: str?) {
    guard let value = input else {
        print("no input provided")
        return
    }
    // 'value' is available as unwrapped str from here on
    print("processing: {value}")
}

The guard keyword combined with pattern matching ensures that preconditions hold, and the else branch must return or break.

Option Handling #

Option<T> represents a value that may or may not be present:

fn find_user(name: str) -> Option<User> {
    // ...
}

// Unwrap with fallback
let user = find_user("Alice").unwrap_or(default_user())

// Map if present
let greeting = find_user("Alice").map(|u| "Hello, {u.name}!")

// Pattern match
match find_user("Bob") {
    Some(u)  => print("Found: {u.name}"),
    None     => print("Not found"),
}

if let for Optional Unwrap

if let Some(user) = find_user("Alice") {
    print("Found: {user.name}")
} else {
    print("Not found")
}

while let for Optional Iteration

while let Some(item) = queue.pop() {
    process(item)
}

Error Propagation Patterns #

Chained Operations with ?

fn pipeline(path: str) -> Result<Report, str> {
    let raw   = read_file(path)?
    let json  = parse_json(raw)?
    let data  = validate(json)?
    let report = generate_report(data)?
    return Result.Ok(report)
}

Converting Between Result and Option

// Option → Result
let r: Result<int, str> = Option.ok_or(find_value(), "not found")

// Result → Option (discards the error)
let o: Option<int> = divide(10, 2).ok()

Summary #

Mechanism Use Case
Result<T, E> Recoverable errors, explicit error types
? operator Propagate errors up the call stack
try/catch/finally Exception-style handling with cleanup
panic Unrecoverable errors, assertion failures
guard Precondition checks with early return
Option<T> + if let Optional values, safe unwrapping
enespt-br