Err (Err v0.2.1)
View SourceErr 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. nil
is 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
/:error
of any size andnil
values. 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
_lazy
variants - ð 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
Summary
Types
An option type representing either some value or none.
A result type representing either success or failure.
Functions
Combines a list of values into a single result.
Chains the result by calling fun
when the value is present.
Wraps value
in an {:error, value}
tuple.
Returns the wrapped value from an {:ok, value}
tuple or raises the provided exception.
Returns the wrapped error from an {:error, reason}
tuple or raises the provided exception.
Flattens a nested result into a single layer.
Checks if a value is an {:error, ...}
result tuple.
Checks if a value is an {:ok, ...}
result tuple.
Checks if a value is "some" (not nil
).
Transforms the success value inside an {:ok, value}
tuple or some value by applying a function to it.
Transforms the error inside an {:error, reason}
tuple by applying a function to it.
Wraps value
in an {:ok, value}
tuple.
Returns the first value if it is present/successful, otherwise returns the second value.
Returns the first value if it is present/successful, otherwise calls the function to compute an alternative value.
Splits a list of results into ok values and error values.
Replaces the value inside an {:ok, value}
tuple with a new value.
Replaces the error inside an {:error, reason}
tuple with a new value.
Replaces the error inside an {:error, reason}
tuple by calling a function.
Replaces the value inside an {:ok, value}
tuple by calling a function.
Returns the wrapped value
or default
when the result is error or value is empty.
Returns the wrapped value or computes it from default_fun
when the result is an error or value
is empty.
Extracts all success values from a list of results.
Types
@type option() :: any() | nil
An option type representing either some value or none.
Can be:
value
- Some value is presentnil
- No value (none)
@type result() :: tuple()
A result type representing either success or failure.
Can be:
{:ok, value}
- A successful result with a value{:error, error}
- A failed result with an error- Any tuple starting with
:ok
or:error
(supports multiple elements)
Functions
Combines a list of values into a single result.
- If all values are
{:ok, value}
, returns{:ok, list_of_values}
. - If any value is an error, returns the first error encountered (fail fast).
- If any value is
nil
, returnsnil
Examples
iex> Err.all([{:ok, 1}, {:ok, 2}, {:ok, 3}])
{:ok, [1, 2, 3]}
iex> Err.all([{:ok, 1}, {:error, :timeout}, {:ok, 3}])
{:error, :timeout}
iex> Err.all([{:ok, 1}, nil, {:ok, 3}])
nil
iex> Err.all([])
{:ok, []}
iex> Err.all([{:ok, "a"}, {:ok, "b"}])
{:ok, ["a", "b"]}
Chains the result by calling fun
when the value is present.
For {:ok, value}
the extracted value (or list of values) is passed to fun
.
Error tuples and nil
are returned unchanged, allowing the pipeline to short-circuit.
Examples
iex> Err.and_then({:ok, 5}, fn num -> num * 2 end)
10
iex> Err.and_then(5, fn num -> num * 2 end)
10
iex> Err.and_then({:ok, :admin, %{id: 1}}, fn [role, user] -> {:ok, %{role: role, user_id: user.id}} end)
{:ok, %{role: :admin, user_id: 1}}
iex> Err.and_then({:error, :timeout}, fn num -> {:ok, num * 2} end)
{:error, :timeout}
iex> Err.and_then(nil, fn value -> {:ok, value} end)
nil
Wraps value
in an {:error, value}
tuple.
Examples
iex> Err.error(:timeout)
{:error, :timeout}
iex> Err.error({:validation_failed, :email})
{:error, {:validation_failed, :email}}
@spec expect!(value(), Exception.t()) :: any()
Returns the wrapped value from an {:ok, value}
tuple or raises the provided exception.
For two-element result tuples ({:ok, value}
) it returns value
. When the tuple contains
additional metadata, it returns the remaining elements as a list.
If the value is {:error, _}
, nil
, or any other value, raises the provided exception.
Examples
iex> Err.expect!({:ok, "config.json"}, RuntimeError.exception("config not found"))
"config.json"
iex> Err.expect!({:ok, :user, %{role: :admin}}, RuntimeError.exception("user not found"))
[:user, %{role: :admin}]
@spec expect_err!(value(), Exception.t()) :: any()
Returns the wrapped error from an {:error, reason}
tuple or raises the provided exception.
For two-element error tuples ({:error, reason}
) it returns reason
. When the tuple contains
additional metadata, it returns the remaining elements as a list.
If the value is {:ok, _}
, nil
, or any other value, raises the provided exception.
Examples
iex> Err.expect_err!({:error, :timeout}, RuntimeError.exception("expected an error"))
:timeout
iex> Err.expect_err!({:error, 404, "Not Found"}, RuntimeError.exception("expected an error"))
[404, "Not Found"]
Flattens a nested result into a single layer.
If the outer result is {:ok, inner}
and inner is also a result tuple,
returns the inner result. Otherwise returns the value unchanged.
Examples
iex> Err.flatten({:ok, {:ok, 1}})
{:ok, 1}
iex> Err.flatten({:ok, {:ok, 1, :meta}})
{:ok, 1, :meta}
iex> Err.flatten({:ok, {:error, :timeout}})
{:error, :timeout}
iex> Err.flatten({:error, :failed})
{:error, :failed}
iex> Err.flatten({:ok, "value"})
{:ok, "value"}
Checks if a value is an {:error, ...}
result tuple.
Returns true
for any tuple starting with :error
, false
otherwise.
Allowed in guard tests.
Examples
iex> Err.is_err({:error, :timeout})
true
iex> Err.is_err({:error, 404, "Not Found"})
true
iex> Err.is_err({:ok, 1})
false
iex> Err.is_err(nil)
false
iex> Err.is_err("error")
false
def my_function(result) when is_err(result)
Checks if a value is an {:ok, ...}
result tuple.
Returns true
for any tuple starting with :ok
, false
otherwise.
Allowed in guard tests.
Examples
iex> Err.is_ok({:ok, 1})
true
iex> Err.is_ok({:ok, 1, 2})
true
iex> Err.is_ok({:error, :timeout})
false
iex> Err.is_ok(nil)
false
iex> Err.is_ok("value")
false
def my_function(result) when is_ok(result)
Checks if a value is "some" (not nil
).
Returns true
for any value except nil
.
Allowed in guard tests.
Examples
iex> Err.is_some(1)
true
iex> Err.is_some("hello")
true
iex> Err.is_some({:ok, 1})
true
iex> Err.is_some(false)
true
iex> Err.is_some(nil)
false
def my_function(value) when is_some(value)
Transforms the success value inside an {:ok, value}
tuple or some value by applying a function to it.
For Result types ({:ok, value}
or {:error, reason}
), applies the function to the value
if it's {:ok, _}
, otherwise returns the error unchanged.
For Option types (nil
or any value), applies the function to the value if it's not nil
,
otherwise returns nil
.
Examples
iex> Err.map({:ok, 5}, fn num -> num * 2 end)
{:ok, 10}
iex> Err.map({:ok, "hello"}, &String.upcase/1)
{:ok, "HELLO"}
iex> Err.map({:error, :timeout}, fn num -> num * 2 end)
{:error, :timeout}
iex> Err.map(nil, fn num -> num * 2 end)
nil
iex> Err.map("hello", &String.upcase/1)
"HELLO"
Transforms the error inside an {:error, reason}
tuple by applying a function to it.
For Result types ({:ok, value}
or {:error, reason}
), applies the function to the error
if it's {:error, _}
, otherwise returns the ok tuple unchanged.
Ignores nil
and non-Result values, returning them unchanged.
Examples
iex> Err.map_err({:error, 404}, fn code -> "HTTP #{code}" end)
{:error, "HTTP 404"}
iex> Err.map_err({:ok, "success"}, fn reason -> "#{reason}_error" end)
{:ok, "success"}
iex> Err.map_err(nil, fn reason -> "#{reason}_error" end)
nil
iex> Err.map_err(404, fn reason -> "#{reason}_error" end)
404
Wraps value
in an {:ok, value}
tuple.
Examples
iex> Err.ok(%{id: 1, email: "john@example.com"})
{:ok, %{email: "john@example.com", id: 1}}
iex> Err.ok({:ok, 100})
{:ok, {:ok, 100}}
Returns the first value if it is present/successful, otherwise returns the second value.
For Result types ({:ok, value}
or {:error, reason}
), returns the first value if it's {:ok, _}
,
otherwise returns the second value.
For Option types (nil
or any value), returns the first value if it's not nil
,
otherwise returns the second value.
Examples
iex> Err.or_else({:ok, "cache.db"}, {:ok, "disk.db"})
{:ok, "cache.db"}
iex> Err.or_else({:ok, "cache.db"}, {:error, :unavailable})
{:ok, "cache.db"}
iex> Err.or_else({:error, :cache_miss}, {:ok, "disk.db"})
{:ok, "disk.db"}
iex> Err.or_else({:error, :cache_miss}, {:error, :disk_full})
{:error, :disk_full}
iex> Err.or_else("primary", "backup")
"primary"
iex> Err.or_else(nil, "backup")
"backup"
Returns the first value if it is present/successful, otherwise calls the function to compute an alternative value.
For Result types ({:ok, value}
or {:error, reason}
), returns the first value if it's {:ok, _}
,
otherwise calls the function with the error reason.
For Option types (nil
or any value), returns the first value if it's not nil
,
otherwise calls the function.
This is the lazy version of or_else/2
- the function is only called when needed.
Examples
iex> Err.or_else_lazy({:ok, "cache.db"}, fn _ -> {:ok, "disk.db"} end)
{:ok, "cache.db"}
iex> Err.or_else_lazy({:error, :cache_miss}, fn _reason -> {:ok, "disk.db"} end)
{:ok, "disk.db"}
iex> Err.or_else_lazy({:error, :timeout}, fn reason -> {:error, "Fallback failed: #{reason}"} end)
{:error, "Fallback failed: timeout"}
iex> Err.or_else_lazy("primary", fn _ -> "backup" end)
"primary"
iex> Err.or_else_lazy(nil, fn _ -> "backup" end)
"backup"
Splits a list of results into ok values and error values.
Returns a tuple {ok_values, error_values}
where:
ok_values
contains all values from{:ok, value}
tupleserror_values
contains all values from{:error, reason}
tuples
Any other value is ignored.
Examples
iex> Err.partition([{:ok, 1}, {:error, "a"}, {:ok, 2}])
{[1, 2], ["a"]}
iex> Err.partition([1, nil])
{[], []}
iex> Err.partition([{:ok, "x"}, {:ok, "y"}])
{["x", "y"], []}
iex> Err.partition([{:error, :timeout}, {:error, :crash}])
{[], [:timeout, :crash]}
iex> Err.partition([])
{[], []}
Replaces the value inside an {:ok, value}
tuple with a new value.
If the result is {:ok, _}
, returns {:ok, new_value}
.
Otherwise returns the original value unchanged.
Examples
iex> Err.replace({:ok, "old"}, "new")
{:ok, "new"}
iex> Err.replace({:error, :timeout}, 999)
{:error, :timeout}
iex> Err.replace(nil, 999)
nil
iex> Err.replace(100, 999)
100
Replaces the error inside an {:error, reason}
tuple with a new value.
If the result is {:error, _}
, returns {:error, new_error}
.
Otherwise returns the original value unchanged.
Examples
iex> Err.replace_err({:error, :timeout}, :network_error)
{:error, :network_error}
iex> Err.replace_err({:error, 404}, :not_found)
{:error, :not_found}
iex> Err.replace_err({:ok, 1}, :error)
{:ok, 1}
iex> Err.replace_err(nil, :error)
nil
Replaces the error inside an {:error, reason}
tuple by calling a function.
If the result is {:error, _}
, calls the function and returns {:error, result}
.
Otherwise returns the original value unchanged without calling the function.
This is the lazy version of replace_err/2
- the function is only called when needed.
Examples
iex> Err.replace_err_lazy({:error, 404}, fn value -> "Status: #{value}" end)
{:error, "Status: 404"}
iex> Err.replace_err_lazy({:ok, 1}, fn _ -> :error end)
{:ok, 1}
iex> Err.replace_err_lazy(nil, fn _ -> :error end)
nil
Replaces the value inside an {:ok, value}
tuple by calling a function.
If the result is {:ok, _}
, calls the function and returns {:ok, result}
.
Otherwise returns the original value unchanged without calling the function.
This is the lazy version of replace/2
- the function is only called when needed.
Examples
iex> Err.replace_lazy({:ok, 1}, fn value -> value + 1 end)
{:ok, 2}
iex> Err.replace_lazy({:error, :timeout}, fn value -> value + 1 end)
{:error, :timeout}
iex> Err.replace_lazy(nil, fn value -> value + 1 end)
nil
Returns the wrapped value
or default
when the result is error or value is empty.
For two-element result tuples ({:ok, value}
) it returns value
. When the tuple
contains additional metadata, it returns the remaining elements as a list.
Accepts nil
, any {:ok, value}
or {:error, reason}
tuple (with or without extra metadata),
and other terms.
Examples
iex> Err.unwrap_or({:ok, "config.json"}, "default.json")
"config.json"
iex> Err.unwrap_or({:ok, :user, %{role: :admin}}, [])
[:user, %{role: :admin}]
iex> Err.unwrap_or({:error, :not_found}, "default.json")
"default.json"
iex> Err.unwrap_or(nil, "default.json")
"default.json"
Returns the wrapped value or computes it from default_fun
when the result is an error or value
is empty.
For successful tuples ({:ok, value}
) the unwrapped value is returned. When the tuple contains
extra data, the remaining elements are returned as a list. For error tuples the extracted value(s)
are passed to default_fun
.
The function receives the extracted value(s): a single value for two-element tuples or a list for larger tuples.
This is the lazy version of unwrap_or/2
- the function is only called when needed.
Examples
iex> Err.unwrap_or_lazy({:ok, "config.json"}, fn _ -> "default.json" end)
"config.json"
iex> Err.unwrap_or_lazy({:ok, :admin, %{perms: [:read]}}, fn _ -> [] end)
[:admin, %{perms: [:read]}]
iex> Err.unwrap_or_lazy({:error, :enoent}, fn reason -> "Error: #{reason}" end)
"Error: enoent"
iex> Err.unwrap_or_lazy(nil, fn _ -> %{role: :guest} end)
%{role: :guest}
Extracts all success values from a list of results.
Returns a list containing all values, except {:error, _}
tuples or nil
.
Examples
iex> Err.values([{:ok, 1}, {:error, :timeout}, {:ok, 2}])
[1, 2]
iex> Err.values([{:ok, 1}, nil, 2])
[1, 2]
iex> Err.values([{:ok, "a"}, {:ok, "b"}])
["a", "b"]
iex> Err.values([{:error, :x}, {:error, :y}])
[]
iex> Err.values([1])
[1]
iex> Err.values([])
[]