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)
}
enespt-br