Err (Err v0.2.1)

View Source

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.
  • 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 and nil 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 with values, split with partition
  • ⚡ 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.

Either a result/0 or an option/0 type.

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

option()

@type option() :: any() | nil

An option type representing either some value or none.

Can be:

  • value - Some value is present
  • nil - No value (none)

result()

@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)

value()

@type value() :: result() | option()

Either a result/0 or an option/0 type.

Functions

all(values)

@spec all([value()]) :: value()

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, returns nil

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"]}

and_then(value, fun)

@spec and_then(value(), (any() -> any())) :: any()

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

error(value)

@spec error(any()) :: result()

Wraps value in an {:error, value} tuple.

Examples

iex> Err.error(:timeout)
{:error, :timeout}

iex> Err.error({:validation_failed, :email})
{:error, {:validation_failed, :email}}

expect!(value, exception)

@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}]

expect_err!(value, exception)

@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"]

flatten(value)

@spec flatten(value()) :: result()

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"}

is_err(value)

(macro)
@spec is_err(any()) :: boolean()

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)

is_ok(value)

(macro)
@spec is_ok(any()) :: boolean()

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)

is_some(value)

(macro)
@spec is_some(any()) :: boolean()

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)

map(value, fun)

@spec map(value(), (any() -> any())) :: 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"

map_err(value, fun)

@spec map_err(value(), (any() -> any())) :: value()

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

message(exception)

@spec message(struct()) :: String.t()

ok(value)

@spec ok(any()) :: result()

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}}

or_else(first, second)

@spec or_else(value(), value()) :: value()

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"

or_else_lazy(value, fun)

@spec or_else_lazy(value(), (any() -> any())) :: value()

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"

partition(results)

@spec partition([value()]) :: {ok_values :: any(), error_values :: any()}

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} tuples
  • error_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([])
{[], []}

replace(error, new_value)

@spec replace(value(), any()) :: value()

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

replace_err(ok, new_error)

@spec replace_err(value(), any()) :: value()

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

replace_err_lazy(ok, fun)

@spec replace_err_lazy(any(), (any() -> any())) :: any()

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

replace_lazy(error, fun)

@spec replace_lazy(value(), (any() -> any())) :: value()

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

unwrap_or(value, default)

@spec unwrap_or(value(), any()) :: any()

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"

unwrap_or_lazy(tuple, default_fun)

@spec unwrap_or_lazy(value(), (any() -> any())) :: any()

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}

values(results)

@spec values([value()]) :: list()

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([])
[]

wrap(exception, opts \\ [])

@spec wrap(
  atom(),
  keyword()
) :: struct()