Err
Err is a tiny library that makes working with tagged {:ok, value} and {:error, reason} tagged tuples more ergonomic and expressive in Elixir.
It follows a simple design to permit using it in existing codebases without changing existing code:
- Tuples
{:ok, _}(of any size) are considered a success result. - Tuples
{:error, _}(of any size) are considered an error result. nilis considered "none".- Any other value is considered "some" value
Inspired by Rust's Result/Option and Gleam's result/option.
Features
- â Composable - Chain operations with
map,and_then,or_else - ð Drop-in compatibility - Handles existing tagged tuples
:ok/:errorof any size andnilvalues. No need to introduce%Result{}structs or special atoms. - âĻ Just functions - No complex custom pipe operators or DSL
- ðŠķ Zero dependencies - Lightweight and fast
- ðĶ List operations - Combine results with
all, extract withvalues, split withpartition - ⥠Lazy evaluation - Avoid computation with
_lazyvariants - ð Transformations - Replace, flatten, and transform results
Installation
Add err to your list of dependencies in mix.exs:
def deps do
[
{:err, "~> 0.2"}
]
end
Usage
Wrap values
iex> Err.ok(42)
{:ok, 42}
iex> Err.error(:timeout)
{:error, :timeout}
Unwrap with defaults
iex> Err.unwrap_or({:ok, "config.json"}, "default.json")
"config.json"
iex> Err.unwrap_or({:error, :not_found}, "default.json")
"default.json"
Lazy unwrapping (function only called when needed)
iex> Err.unwrap_or_lazy({:error, :enoent}, fn reason -> "Error: #{reason}" end)
"Error: enoent"
Transform success values
iex> Err.map({:ok, 5}, fn num -> num * 2 end)
{:ok, 10}
Transform error values
iex> Err.map_err({:error, :timeout}, fn reason -> "#{reason}_error" end)
{:error, "timeout_error"}
Chain operations
iex> Err.and_then({:ok, 5}, fn num -> {:ok, num * 2} end)
{:ok, 10}
Flatten nested results
iex> Err.flatten({:ok, {:ok, 1}})
{:ok, 1}
Eager fallback
iex> Err.or_else({:error, :cache_miss}, {:ok, "disk.db"})
{:ok, "disk.db"}
Lazy fallback
Err.or_else_lazy({:error, :cache_miss}, fn _reason ->
{:ok, load_from_disk()}
end)
Combine results (fail fast)
iex> Err.all([{:ok, 1}, {:ok, 2}, {:ok, 3}])
{:ok, [1, 2, 3]}
iex> Err.all([{:ok, 1}, {:error, :timeout}])
{:error, :timeout}
Extract ok values
iex> Err.values([{:ok, 1}, {:error, :x}, {:ok, 2}])
[1, 2]
Split into ok and error lists
iex> Err.partition([{:ok, 1}, {:error, "a"}, {:ok, 2}])
{[1, 2], ["a"]}
Check if result is ok
def process(result) when Err.is_ok(result) do
# handle ok
end
Check if result is error
def process(result) when Err.is_err(result) do
# handle error
end
Real-World Example
def fetch_user_profile(user_id) do
user_id
|> fetch_user()
|> Err.and_then(&load_profile/1)
|> Err.and_then(&enrich_with_stats/1)
|> Err.or_else_lazy(fn _error ->
{:ok, %{name: "Guest", stats: %{}}}
end)
end