Nesta página
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!)
}Defer on Panic¶
defer also fires when the function exits via panic(...). The runtime
wraps the body in a per-function trap so the cleanup runs before the
panic propagates to the enclosing try / catch (or the top-level
panic handler):
fn boom() {
defer print("cleanup") // prints
panic("oops") // panic propagates AFTER cleanup
}
try { boom() } catch e { print("caught") }
// Output:
// cleanup
// caughtThis applies to defers in any block scope — a defer inside an inner
{ ... } fires when a panic skips over its enclosing block, then the
function-level defers fire, then the panic re-emerges.
defer_ok and defer_err — Conditional Cleanup¶
The triad lets you split cleanup by exit path without juggling a boolean flag yourself. All three share one LIFO stack per block.
| Keyword | Fires when the block exits via… |
|---|---|
defer |
any exit — fall-through, return, ?, panic |
defer_ok |
success only — return Ok, fall-through, break |
defer_err |
failure only — return Err, ?, panic |
use std::Result
fn transfer(from: Account, to: Account, amount: Money) -> Result<(), Error> {
let tx = db.begin()?
defer_err tx.rollback() // only if something below fails
defer_ok tx.commit() // only on the success path
defer db.log_metrics() // always runs
debit(tx, from, amount)?
credit(tx, to, amount)?
Result.Ok(())
}The compiler decides at run-time whether each registered cleanup fires,
based on the function's exit value. defer_ok / defer_err accept an
optional |binding| to capture the returned value:
fn handle(req: Request) -> Result<Response, ApiError> {
defer_ok |r: Response| metrics.success(req.trace, r.status)
defer_err |e: ApiError| metrics.failure(req.trace, e.code)
process(req)
}Inside a Result<T, E> function the binding receives the inner T or
E, not the wrapping Result. For non-Result functions and panic
payloads the value flows through unchanged.
Restrictions in the Defer Body¶
The compiler rejects (E_DEFER_003) control-flow constructs that
would jump out of the cleanup scope:
fn bad() {
defer return // E_DEFER_003: `return` not allowed in defer body
defer { foo()? } // E_DEFER_003: `?` not allowed in defer body
defer { break } // E_DEFER_003: `break` not allowed in defer body
}Use a nested try if you need to swallow a fallible call inside a
cleanup:
defer_err {
try { rollback() } catch _ {
log.error("rollback failed; check transaction log")
}
}Suppressed Errors¶
If a cleanup itself panics on the error path, the original error is
preserved and the cleanup failure is attached as .suppressed:
use std::Result
struct AppError { message: str }
fn op() -> Result<int, AppError> {
defer_err |_e| panic("cleanup-fail")
Result.Err(AppError { message: "primary" })
}
match op() {
Result.Ok(v) => print("ok: {v}"),
Result.Err(e) => {
print(e.message) // "primary" — root cause kept
print(e.suppressed[0]) // "cleanup-fail" — attached
},
}The mechanism only attaches when the payload is a struct/table —
primitive errors (str, int) have nowhere to hang the field.
Top-Level Defer¶
Only the plain defer is allowed at module scope; it fires once at
program exit in LIFO order:
defer print("program ended") // runs last
print("hello")defer_ok / defer_err at module scope are rejected with
E_DEFER_004 — the module's "exit value" isn't well-defined for the
ok/err distinction. Use on panic { ... }
for global failure cleanup instead.
Diagnostics Reference¶
| Code | When |
|---|---|
E_DEFER_003 |
return / break / continue / ? inside a defer body |
E_DEFER_004 |
defer_ok / defer_err used at module top level |
E_DEFER_007 |
defer_ok |v| binding in a function with unit return |
E_DEFER_009 |
typed binding incompatible with the enclosing return type |
W_DEFER_006 |
defer_err in a function that statically cannot reach an error exit |
See specs/defer-ok-err.md for the full
specification including the lowering details.
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¶
use std::Result
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. It checks a boolean condition; it
does not bind a pattern:
fn send_email(to: str?, body: str) {
guard to != nil else {
print("no recipient")
return
}
let address = to ?? ""
print("send to {address}: {body}")
}
send_email("ada@example.com", "hello")
send_email(nil, "ignored")Several conditions can be checked in sequence. After a nil check, ??
extracts the optional value:
fn process_user(name: str?, active: bool) {
guard name != nil else { return }
guard active else { return }
let value = name ?? ""
print("Processing: {value}")
}
process_user("Ada", true)
process_user("Bo", false)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:
'outer: for i in 0..5 {
for j in 0..5 {
if i + j > 6 {
break 'outer // exits both loops
}
print("{i},{j}")
}
}'outer: for i in 0..5 {
for j in 0..5 {
if j == 2 {
continue 'outer // skips to next iteration of outer loop
}
print("{i},{j}")
}
}Range Expressions¶
Ranges are first-class values in Zolo:
use std::Array
use std::Iter
// 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¶
use std::Iter
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")
}
let maybe_user = await db.find_user(parsed_id)
guard maybe_user != nil else {
return Response.not_found("user not found")
}
let user = maybe_user ?? panic("guard invariant violated")
// Happy path — clean and flat
let data = await fetch_user_data(user)
return Response.ok(data)
}