error_union

Package VersionHex 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/.