# error_union

[![Package Version](https://img.shields.io/hexpm/v/error_union)](https://hex.pm/packages/error_union)
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](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/>.
