Guards and early return
Returning early when a precondition fails is the most readable form of
validation in Zolo: check at the top, return Result.Err(...) immediately,
and the rest of the function executes only on the happy path — no nested
if/else.
Combined with ?, a validation pipeline becomes linear:
Validation with early return, pipeline with ?, and optional with nil return.
// Feature: Early-return and argument validation
// Syntax: `if cond { return ... }` at the start of the function
// When to use: validate preconditions and simplify the happy path.
use std::Result
// -- Validation as Result — no panic ---------------------------
fn validate_age(n: int) -> Result<int, str> {
if n < 0 {
return Result.Err("negative age")
}
if n > 150 {
return Result.Err("implausible age")
}
return Result.Ok(n)
}
fn describe(r: Result<int, str>) {
match r {
Result::Ok(v) => print("ok: {v}"),
Result::Err(e) => print("error: {e}"),
}
}
describe(validate_age(30)) // ok: 30
describe(validate_age(-1)) // error: negative age
describe(validate_age(200)) // error: implausible age
// -- Validation pipeline with `?` -----------------------------
fn validate_email(s: str) -> Result<str, str> {
if s.len() == 0 {
return Result.Err("empty email")
}
if !s.contains("@") {
return Result.Err("email missing @")
}
return Result.Ok(s)
}
fn validate_name(s: str) -> Result<str, str> {
if s.len() < 2 {
return Result.Err("name too short")
}
return Result.Ok(s)
}
struct User {
name: str,
email: str,
}
fn create_user(name: str, email: str) -> Result<User, str> {
let n = validate_name(name)? // propagates error
let e = validate_email(email)? // propagates error
return Result.Ok(User { name: n, email: e })
}
fn describe_user(r: Result<User, str>) {
match r {
Result::Ok(u) => print("user: {u.name} <{u.email}>"),
Result::Err(msg) => print("error: {msg}"),
}
}
describe_user(create_user("Alice", "[email protected]"))
// expected: user: Alice <[email protected]>
describe_user(create_user("A", "[email protected]"))
// expected: error: name too short
describe_user(create_user("Alice", "invalid"))
// expected: error: email missing @
// -- Optional + early return ----------------------------------
fn first_vowel(s: str) -> str? {
for c in s.chars() {
if c == "a" || c == "e" || c == "i" || c == "o" || c == "u" {
return c
}
}
return nil
}
print(first_vowel("brown") ?? "(none)") // o
print(first_vowel("xyz") ?? "(none)") // (none)
The create_user pattern shows the core of the technique: each validator
returns Result<_, str>, and ? propagates the first error found without
accumulating nested match. The caller sees a single descriptive failure point.
Use guards for input invariants — data arriving from the outside world (forms, APIs, files). Inside internal logic, where you control the types, prefer
panicfor broken contracts.
Challenge
Add a third validation to create_user that rejects emails longer than 100
characters. Test with a long string and verify that the correct error is
propagated.