Advanced Control Flow
Beyond basic if/else, for, while, and match, Zolo provides several advanced control flow mechanisms.
Defer #
defer schedules an expression to run when the enclosing scope exits, regardless of how it exits (normal flow, early return, error):
fn process_file(path: str) {
let file = open(path)
defer file.close() // guaranteed to run when function exits
let data = file.read()
process(data)
// file.close() runs here automatically
}
Multiple Defers #
Multiple defer statements run in reverse order (LIFO):
fn multi_resource() {
let db = db_connect()
defer db.disconnect() // runs third
let cache = cache_open()
defer cache.close() // runs second
let lock = acquire_lock()
defer release_lock(lock) // runs first
// use resources...
}
Defer with Early Return #
fn validate_and_process(data: str?) {
let conn = open_connection()
defer conn.close() // always runs, even if we return early
if data == nil {
print("no data")
return // conn.close() still runs here
}
process(data!)
}
Let-Else #
let-else is a pattern that binds a value from a pattern match, or runs an else block (which must diverge — i.e., return, break, continue, or panic):
fn process_config(raw: str?) {
let Some(config) = raw else {
print("no config provided")
return
}
// 'config' is available here as str
parse(config)
}
With Enum Variants #
fn handle_result(r: Result<int, str>) {
let Result.Ok(value) = r else {
print("operation failed")
return
}
print("got value: {value}")
}
Compared to if let
let-else is the inverse of if let. Use if let when the happy path is nested; use let-else to bail out early and keep the happy path flat:
// if let — happy path is nested
if let Some(value) = get_value() {
process(value)
}
// let-else — happy path is flat (preferred for early returns)
let Some(value) = get_value() else { return }
process(value)
While Let #
while let loops as long as a pattern matches:
// Process items until the queue is empty
while let Some(item) = queue.pop() {
process(item)
}
// Consume an iterator manually
let iter = some_iter()
while let Some(val) = iter.next() {
print(val)
}
With Result #
// Read lines until error
while let Result.Ok(line) = reader.next_line() {
handle(line)
}
Guard #
guard provides an early-exit precondition with a bound variable available after the check:
fn send_email(to: str?, body: str) {
guard let address = to else {
print("no recipient")
return
}
// 'address' is str here (unwrapped)
email_client.send(address, body)
}
guard works with any pattern:
fn process_user(data: {str: any}?) {
guard let Some(d) = data else { return }
guard let name = d["name"] as str? else { return }
print("Processing: {name}")
}
Loop with Break Value #
loop can return a value via break:
let result = loop {
let input = read_input()
if is_valid(input) {
break input // loop evaluates to 'input'
}
print("invalid, try again")
}
print("You entered: {result}")
Labeled Breaks and Continues #
For nested loops, use labels to break or continue an outer loop:
39;outer: for i in 0..5 {
for j in 0..5 {
if i + j > 6 {
break 39;outer // exits both loops
}
print("{i},{j}")
}
}
39;outer: for i in 0..5 {
for j in 0..5 {
if j == 2 {
continue 39;outer // skips to next iteration of outer loop
}
print("{i},{j}")
}
}
Range Expressions #
Ranges are first-class values in Zolo:
// Exclusive range (0 to 9)
for i in 0..10 {
print(i)
}
// Inclusive range (0 to 10)
for i in 0..=10 {
print(i)
}
// Infinite range (lazy — use with Iter.take)
let first5 = (0..)
|> Iter.take(5)
|> Iter.collect()
print(first5) // [0, 1, 2, 3, 4]
// From-start range (up to but not including 10)
let head = Array.filter([1, 2, 3, 4, 5, 6], |i| i in ..3)
// Ranges in match
fn classify(n: int) -> str {
match n {
0 => "zero",
1..=9 => "single digit",
10..=99 => "double digit",
_ => "large",
}
}
Range as Value #
let r = 1..=5
print(r.contains(3)) // true
print(r.contains(6)) // false
// Collect a range
let nums = (1..=5) |> Iter.collect()
print(nums) // [1, 2, 3, 4, 5]
Comprehensive Pattern Example #
Combining defer, let-else, guard, and try/catch for robust code:
async fn handle_request(req: Request) -> Response {
let conn = db.connect()
defer conn.close()
// Validate input early
let Some(user_id) = req.params["user_id"] else {
return Response.bad_request("missing user_id")
}
let parsed_id = try {
parse_int(user_id)
} catch _ {
return Response.bad_request("invalid user_id")
}
guard let user = await db.find_user(parsed_id) else {
return Response.not_found("user not found")
}
// Happy path — clean and flat
let data = await fetch_user_data(user)
return Response.ok(data)
}