error_union

Package Version Hex Docs

Layer-aware error handling for Gleam.

Chain fallible steps with use <- try(...) just like gleam/result.try — but each step keeps its own error type, a failure remembers which step it came from, and you deal with every layer’s error in one place.

gleam add error_union

result.try vs error_union.try

Say you have two fallible steps whose errors have different types:

import gleam/int

// step 1: parses an int, error type is Nil
fn parse_int(s: String) -> Result(Int, Nil) {
  int.parse(s)
}

// step 2: checks a number is positive, error type is String
fn ensure_positive(n: Int) -> Result(Int, String) {
  case n > 0 {
    True -> Ok(n)
    False -> Error("must be positive")
  }
}

Official result.try: all steps must share one error type

import gleam/result

pub fn parse_and_check(s: String) -> Result(Int, String) {
  use a <- result.try(
    parse_int(s) |> result.map_error(fn(_) { "not an int" }), // Nil -> String
  )
  use b <- result.try(ensure_positive(a))
  Ok(b)
}

Every step must share the same error type, so step 1 needs a manual map_error shim — and the resulting Error("not an int") is just a String that tells you nothing about which step failed or what its real error was.

error_union.try: each layer keeps its own error type

import error_union.{deal, try}

pub fn parse_and_check(s: String) {
  use a <- try(parse_int(s))       // step 1 error stays Nil
  use b <- try(ensure_positive(a)) // step 2 error stays String
  Ok(b)
}

pub fn handle(s: String) -> String {
  case parse_and_check(s) {
    Ok(n) -> "ok: " <> int.to_string(n)
    Error(eu) -> {
      // one `use <- deal` per `use <- try` layer
      use eu <- deal(eu, fn(_) { "not an int" })          // layer 1
      use eu <- deal(eu, fn(e) { "not positive: " <> e }) // layer 2
      eu //unreachable  required by the type checker
    }
  }
}

pub fn main() {
  handle("42") // -> "ok: 42"
  handle("0")  // -> "not positive: must be positive"
  handle("x")  // -> "not an int"
}

How it works

Eu(a, b) is a two-slot error bag:

try(r, f) wraps failures into Eu:

deal(eu, current, sequential) unwraps one layer: an error in slot a goes to current, an error in slot b goes to sequential — which is where the next use <- deal picks it up. That’s why the number of use <- deal lines matches the number of use <- try lines.

Note: use eu <- deal(eu, ...) shadows eu so each layer keeps unwrapping. Writing the final eu value is required by the type checker — it can only be reached if the whole chain somehow returns Error at the very end.

Throwing your own value at the end of the chain

The last block of a use chain doesn’t have to end in Ok — return Error(anything) and that value lands in the innermost slot, recoverable once deal has unwrapped every layer.

import error_union.{deal, try}
import gleam/int

pub fn main() {
  let result = {
    use a <- try(Ok(1)) // this layer passes
    use b <- try(Ok(2)) // and this one too
    Error(#("boom", a + b)) // throw your own value instead
  }

  let handled = case result {
    Ok(_) -> "unexpected ok"
    Error(eu) -> {
      use eu <- deal(eu, fn(_) { "unreachable" })
      use eu <- deal(eu, fn(_) { "unreachable" })
      eu.0 <> " err value is " <> int.to_string(eu.1) // eu : #(String, Int)
    }
  }
  // handled == "boom err value is 3"
}

Development

gleam test  # Run the tests

Further documentation can be found at https://error-union.hexdocs.pm/.

Search Document