Skip to content

Result (std::Result)

std::Result represents either a success (Result.Ok(value)) or a failure (Result.Err(message)). Functions that can fail return a Result instead of throwing exceptions — the caller decides how to react.

Ok and Err

The two constructors and the predicates is_ok / is_err:

A divide function that returns Ok or Err depending on the divisor.

01-ok-and-err.zolo
Playground
// Feature: Result.Ok / Result.Err — the two variants of the type
// When to use: representing success or failure without using exceptions.

use std::Result

let ok = Result.Ok(42)
let err = Result.Err("failed")

// is_ok / is_err — quick checks.
print(ok.is_ok())  // expected: true
print(ok.is_err())  // expected: false
print(err.is_ok())  // expected: false
print(err.is_err())  // expected: true

// Function that returns a Result.
fn divide(a: int, b: int) -> Result<int, str> {
  if b == 0 {
    return Result.Err("division by zero")
  }
  return Result.Ok(a / b)
}

let r1 = divide(10, 2)
let r2 = divide(10, 0)
print(r1.is_ok())  // expected: true
print(r2.is_err())  // expected: true

Unwrap

unwrap() extracts the value from an Ok — panics on Err. Use unwrap_or(default) to provide a safe fallback:

unwrap_or never panics; unwrap_err extracts the error message.

02-unwrap.zolo
Playground
// Feature: .unwrap / .unwrap_or / .unwrap_err — extracting the value
// When to use: pulling the inner value; unwrap_or avoids panic on Err.

use std::Result

let ok = Result.Ok(42)
let err = Result.Err("oops")

// unwrap — returns the value; PANICS on Err.
print(ok.unwrap())  // expected: 42

// unwrap_or — safe default when Err.
print(err.unwrap_or(0))  // expected: 0
print(ok.unwrap_or(999))  // expected: 42

// unwrap_err — returns the error; PANICS on Ok.
print(err.unwrap_err())  // expected: oops

// Safe pattern: test first.
fn show(r: Result<int, str>) {
  if r.is_ok() {
    print("ok: {r.unwrap()}")
  } else {
    print("err: {r.unwrap_err()}")
  }
}

show(Result.Ok(10))  // expected: ok: 10
show(Result.Err("failure"))  // expected: err: failure

Map

map transforms the inner value of an Ok without leaving the Result. An Err passes through untouched. map_err does the same for the error side:

Chaining map for multiple transformations.

03-map.zolo
Playground
// Feature: Result.map / Result.map_err — transform within a Result

// When to use: applying a transformation to the value without unwrapping.


use std::Result

let ok = Result.Ok(10)
let err = Result.Err("failure")

// map — transforms the inner value when Ok; passes Err through unchanged.

let doubled = ok.map(|x| x * 2)
print(doubled.unwrap())  // expected: 20


let still_err = err.map(|x| x * 2)
print(still_err.is_err())  // expected: true

print(still_err.unwrap_err())  // expected: failure


// map_err — transforms the error when Err; passes Ok through unchanged.

let prefixed = err.map_err(|e| "ERROR: " + e)
print(prefixed.unwrap_err())  // expected: ERROR: failure


let ok2 = ok.map_err(|e| "never")
print(ok2.unwrap())  // expected: 10


// Chain map for multiple transformations.

let r = Result.Ok(5)
let r2 = r.map(|x| x + 1)
let r3 = r2.map(|x| x * 10)
print(r3.unwrap())  // expected: 60

And Then

and_then chains operations that themselves return a Result. The first Err stops the chain and propagates:

Two steps that can fail; and_then eliminates manual checking.

04-and-then.zolo
Playground
// Feature: Result.and_then — chain operations that return a Result

// When to use: pipelines with multiple failing steps; each callback

// returns a fresh Result.


use std::Result

fn parse_int(s: str) -> Result<int, str> {
  if s == "1" { return Result.Ok(1) }
  if s == "2" { return Result.Ok(2) }
  if s == "3" { return Result.Ok(3) }
  return Result.Err("parse error: {s}")
}

fn double_if_even(n: int) -> Result<int, str> {
  if n % 2 == 0 {
    return Result.Ok(n * 2)
  }
  return Result.Err("not even: {n}")
}

// Happy path — all Ok.

let r1 = parse_int("2").and_then(double_if_even)
print(r1.is_ok())  // expected: true

print(r1.unwrap())  // expected: 4


// Failure in the 1st step — Err propagates.

let r2 = parse_int("z").and_then(double_if_even)
print(r2.is_err())  // expected: true


// Failure in the 2nd step.

let r3 = parse_int("1").and_then(double_if_even)
print(r3.is_err())  // expected: true

print(r3.unwrap_err())  // expected: not even: 1

The ? Operator

? after a Result expression extracts the Ok or immediately returns the Err to the caller — equivalent to if is_err { return } without the noise:

add with ? vs. add_v1 without ? — identical in behaviour, different in clarity.

05-try-operator.zolo
Playground
// Feature: The `?` operator — propagates Err automatically
// When to use: clean code in pipelines; equivalent to if-let-Err-return.

use std::Result

fn parse_int(s: str) -> Result<int, str> {
  if s == "10" { return Result.Ok(10) }
  if s == "5" { return Result.Ok(5) }
  return Result.Err("parse: {s}")
}

// Without `?`: every call needs to be checked manually.
fn add_v1(a: str, b: str) -> Result<int, str> {
  let ra = parse_int(a)
  if ra.is_err() { return ra }
  let rb = parse_int(b)
  if rb.is_err() { return rb }
  return Result.Ok(ra.unwrap() + rb.unwrap())
}

// With `?`: each `?` extracts the Ok or returns the Err immediately.
fn add(a: str, b: str) -> Result<int, str> {
  let x = parse_int(a)?
  let y = parse_int(b)?
  return Result.Ok(x + y)
}

// Happy path.
let r1 = add("10", "5")
print(r1.unwrap())  // expected: 15

// Failure — first Err propagates.
let r2 = add("oops", "5")
print(r2.is_err())  // expected: true
print(r2.unwrap_err())  // expected: parse: oops

// Failure on the second call.
let r3 = add("10", "xyz")
print(r3.unwrap_err())  // expected: parse: xyz

// Both versions are equivalent.
print(add_v1("10", "5").unwrap())  // expected: 15

Challenge

Write a function sqrt_positive(n: int) -> Result<float, str> that returns Err when n < 0 and Ok(sqrt(n)) otherwise. Then compose it with and_then to compute the sum of two square roots.

enespt-br