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 |