Skip to content

Result Combinators

Combinators let you compose operations on Result without intermediate match or unwrap. Each method takes a closure and returns a new Result:

Method When to use
.map(f) Transforms the Ok value; Err passes through unchanged
.map_err(f) Transforms the error; Ok passes through unchanged
.and_then(f) Chains an operation that also returns Result
.or_else(f) Tries to recover from an Err

map, map_err, and_then, or_else and a full chained pipeline.

04-result-combinators.zolo
Playground
// Feature: Result combinators — map, map_err, and_then, or_else
// Syntax: `r.map(fn)`, `r.map_err(fn)`, `r.and_then(fn)`, `r.or_else(fn)`
// When to use: transform/recover without unwrapping.

use std::Result

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

// -- map: transforms the Ok value, leaves Err passthrough -----
let r1 = divide(10, 2).map(|v| v * 10)
print(r1.unwrap())  // 50

let r2 = divide(10, 0).map(|v| v * 10)
print(r2.is_err())  // true (map didn't touch the error)

// -- map_err: transforms the error, leaves Ok passthrough -----
let r3 = divide(10, 0).map_err(|e| "math: {e}")
print(r3.unwrap_err())  // math: division by zero

let r4 = divide(10, 2).map_err(|e| "ignored: {e}")
print(r4.unwrap())  // 5 (map_err doesn't touch Ok)

// -- and_then: chains an operation that returns Result --------
fn safe_double(n: int) -> Result<int, str> {
  if n > 1000 {
    return Result.Err("overflow")
  }
  return Result.Ok(n * 2)
}

let r5 = divide(20, 4).and_then(safe_double)
print(r5.unwrap())  // 10

// Err stops the chain at the first error:
let r6 = divide(20, 0).and_then(safe_double)
print(r6.is_err())  // true (safe_double was never called)

// -- or_else: recovers from Err -------------------------------
let r7 = divide(10, 0).or_else(|_| Result.Ok(0))
print(r7.unwrap())  // 0

let r8 = divide(10, 2).or_else(|_| Result.Ok(999))
print(r8.unwrap())  // 5 (Ok passes through)

// -- Full pipeline --------------------------------------------
fn pipeline(x: int) -> Result<int, str> {
  return divide(x, 2).and_then(safe_double).map(|v| v + 1)
}

print(pipeline(10).unwrap())  // (10/2)*2 + 1 = 11
print(pipeline(0).unwrap())  // 0/2=0, *2=0, +1 = 1

.and_then stops at the first failure — useful for pipelines where each step depends on the success of the previous one. .or_else does the opposite: it recovers the flow when there is an error.

Challenge

Add a second .and_then to the pipeline that rejects values greater than 100. What happens with pipeline(500)?

enespt-br