Results, Options, and Error Handling

Errors as Values

Parsing and Dividing Safely

i
fn parse_int(s: String) -> Result(Int, String) {
  case int.parse(s) {
    Ok(n) -> Ok(n)
    Error(_) -> Error("not an integer: " <> s)
  }
}
i
fn safe_divide(a: Int, b: Int) -> Result(Int, String) {
  case b {
    0 -> Error("division by zero")
    _ -> Ok(a / b)
  }
}
i
Ok(42)
Error("not an integer: not a number")
Ok(5)
Error("division by zero")

Transforming Results

i
  io.println(string.inspect(ok_int(42) |> result.map(fn(x) { x + 1 })))
  io.println(string.inspect(error_str("oops") |> result.map(fn(x) { x + 1 })))
i
Ok(43)
Error("oops")

The Option Type

i
fn option_inspect(opt: Option(Int)) -> String {
  case opt {
    Some(n) -> "got " <> int.to_string(n)
    None -> "nothing"
  }
}
i
fn first_positive(nums: List(Int)) -> Option(Int) {
  case nums {
    [] -> None
    [x, .._rest] if x > 0 -> Some(x)
    [_, ..rest] -> first_positive(rest)
  }
}
i
"got 42"
"nothing"
Some(99)
None
Some(3)
None

Chaining with use

i
  let result = {
    use x <- result.try(parse_int("10"))
    use y <- result.try(parse_int("20"))
    use z <- result.try(safe_divide(x + y, 3))
    Ok(z * 2)
  }
  io.println(string.inspect(result))
case parse_int("10") {
  Ok(x) -> ...rest of block...
  Error(e) -> Error(e)
}
i
  let fail = {
    use x <- result.try(parse_int("10"))
    use y <- result.try(parse_int("bad"))
    use z <- result.try(safe_divide(x + y, 3))
    Ok(z * 2)
  }
  io.println(string.inspect(fail))
i
Ok(20)
Error("not an integer: bad")

Guidelines for Handling Errors

Check Understanding

When should you use Option instead of Result?
How does the use expression compare to Python's try/except?

In Python, try/except catches exceptions that are raised anywhere in the block, regardless of whether the function signature mentions them. Gleam's use only short-circuits when a function explicitly returns an Error variant. This means you always know from the type whether a function can fail, and the use block's short-circuiting is visible in the code structure. The practical difference is that Python's exceptions are invisible in type signatures, while Gleam's Result values are not.

Exercises

Safe division (5 minutes)

Write safe_divide(a: Int, b: Int) -> Result(Int, String) that returns Error("division by zero") when b is 0 and Ok(a / b) otherwise.

Mapping over results (5 minutes)

Write a function that takes a List(String) and returns a List(Int) containing only the elements that parse successfully as integers. Use int.parse, list.filter_map, or a combination of list.map and list.filter.

Chaining with use (15 minutes)

Write three functions that each return Result(Int, String): parse_age(s: String), validate_age(n: Int) (returns Error if n < 0 or n > 150), and to_birth_year(age: Int) that subtracts from the current year. Chain them with use so that (for example) "26" produces Ok(2000) and "-5" produces an error at the validation step.

First matching element (10 minutes)

Write find(lst: List(a), pred: fn(a) -> Bool) -> Option(a) that returns Some(x) for the first element where pred(x) is True, or None if no element matches. Write it using recursion with a case expression.