Packages
error_union
1.0.0
Layer-aware error handling for Gleam. Chain use <- try(...) like result.try, each layer keeps its own error type, deal with every layer in one place.
Current section
Files
Jump to
Current section
Files
error_union
README.md
README.md
# error_union
[](https://hex.pm/packages/error_union)
[](https://error-union.hexdocs.pm/)
**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.
```sh
gleam add error_union
```
## `result.try` vs `error_union.try`
Say you have two fallible steps whose errors have different types:
```gleam
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
```gleam
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
```gleam
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:
- slot `a` — the error of **this** step (the `Result` you passed in)
- slot `b` — the error of the **following** steps (usually another nested `Eu`)
`try(r, f)` wraps failures into `Eu`:
- `r` is `Error(e)` → `Eu([e], [])` — *this* step failed
- `r` is `Ok` but `f` fails → `Eu([], [e])` — a *later* step failed
`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.
```gleam
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
```sh
gleam test # Run the tests
```
Further documentation can be found at <https://error-union.hexdocs.pm/>.