Skip to content

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.

09-guards-and-early-return.zolo
Playground
// 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 panic for 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.

enespt-br