Current section
5 Versions
Jump to
Current section
5 Versions
Compare versions
4
files changed
+273
additions
-9
deletions
| @@ -1,5 +1,10 @@ | |
| 1 1 | # Changelog |
| 2 2 | |
| 3 | + ## 0.2.1 - 2025-10-09 |
| 4 | + |
| 5 | + ### Added |
| 6 | + - Add usage-rules.md for LLMs |
| 7 | + |
| 3 8 | ## 0.2.0 - 2025-10-09 |
| 4 9 | |
| 5 10 | ### Added |
| @@ -2,13 +2,14 @@ | |
| 2 2 | [{<<"Changelog">>,<<"https://hexdocs.pm/err/changelog.html">>}, |
| 3 3 | {<<"GitHub">>,<<"https://github.com/leandrocp/err">>}]}. |
| 4 4 | {<<"name">>,<<"err">>}. |
| 5 | - {<<"version">>,<<"0.2.0">>}. |
| 5 | + {<<"version">>,<<"0.2.1">>}. |
| 6 6 | {<<"description">>, |
| 7 7 | <<"Tiny library that makes working with tagged {:ok, value} and {:error, reason} tagged tuples more ergonomic and expressive.">>}. |
| 8 8 | {<<"elixir">>,<<"~> 1.15">>}. |
| 9 9 | {<<"files">>, |
| 10 10 | [<<"mix.exs">>,<<"lib">>,<<"lib/err">>,<<"lib/err/generic_error.ex">>, |
| 11 | - <<"lib/err.ex">>,<<"README.md">>,<<"LICENSE">>,<<"CHANGELOG.md">>]}. |
| 11 | + <<"lib/err.ex">>,<<"README.md">>,<<"LICENSE">>,<<"CHANGELOG.md">>, |
| 12 | + <<"usage-rules.md">>]}. |
| 12 13 | {<<"app">>,<<"err">>}. |
| 13 14 | {<<"licenses">>,[<<"MIT">>]}. |
| 14 15 | {<<"requirements">>,[]}. |
| @@ -2,7 +2,7 @@ defmodule Err.MixProject do | |
| 2 2 | use Mix.Project |
| 3 3 | |
| 4 4 | @source_url "https://github.com/leandrocp/err" |
| 5 | - @version "0.2.0" |
| 5 | + @version "0.2.1" |
| 6 6 | |
| 7 7 | def project do |
| 8 8 | [ |
| @@ -44,12 +44,13 @@ defmodule Err.MixProject do | |
| 44 44 | Changelog: "https://hexdocs.pm/err/changelog.html", |
| 45 45 | GitHub: @source_url |
| 46 46 | }, |
| 47 | - files: [ |
| 48 | - "mix.exs", |
| 49 | - "lib", |
| 50 | - "README.md", |
| 51 | - "LICENSE", |
| 52 | - "CHANGELOG.md" |
| 47 | + files: ~w[ |
| 48 | + mix.exs |
| 49 | + lib |
| 50 | + README.md |
| 51 | + LICENSE |
| 52 | + CHANGELOG.md |
| 53 | + usage-rules.md |
| 53 54 | ] |
| 54 55 | ] |
| 55 56 | end |
| @@ -0,0 +1,257 @@ | |
| 1 | + # Err - Usage Rules for AI Agents |
| 2 | + |
| 3 | + Err is an Elixir library providing Result and Option types inspired by Rust, offering explicit error handling without exceptions. |
| 4 | + |
| 5 | + ## When to Use Err |
| 6 | + |
| 7 | + **Use Err when:** |
| 8 | + - Building domain logic with explicit error states |
| 9 | + - Chaining operations that may fail |
| 10 | + - Working with values that may be absent (Option type) |
| 11 | + - You want type-safe error handling in pipelines |
| 12 | + - Converting between `{:ok, value}` and `{:error, reason}` patterns |
| 13 | + |
| 14 | + **Don't use Err when:** |
| 15 | + - Working with existing code that expects standard tuples (use standard pattern matching) |
| 16 | + - The overhead of wrapping/unwrapping isn't justified |
| 17 | + - You need traditional try/catch exception handling |
| 18 | + |
| 19 | + ## Core Types |
| 20 | + |
| 21 | + ```elixir |
| 22 | + @type result :: tuple() # {:ok, value} | {:error, reason} |
| 23 | + @type option :: any() | nil |
| 24 | + @type value :: result() | option() |
| 25 | + ``` |
| 26 | + |
| 27 | + **Important:** Err supports multi-element tuples like `{:ok, value, metadata}` and `{:error, reason, details}`. |
| 28 | + |
| 29 | + ## Common Patterns |
| 30 | + |
| 31 | + ### Creating Results and Options |
| 32 | + |
| 33 | + ```elixir |
| 34 | + # Wrap values |
| 35 | + Err.ok(user) # => {:ok, user} |
| 36 | + Err.error(:not_found) # => {:error, :not_found} |
| 37 | + ``` |
| 38 | + |
| 39 | + ### Unwrapping Values Safely |
| 40 | + |
| 41 | + ```elixir |
| 42 | + # With defaults |
| 43 | + Err.unwrap_or({:ok, config}, default_config) # => config |
| 44 | + Err.unwrap_or({:error, _}, default_config) # => default_config |
| 45 | + |
| 46 | + # With exceptions (let it crash when a value is expected) |
| 47 | + Err.expect!({:ok, user}, RuntimeError.exception("user required")) # => user |
| 48 | + Err.expect!({:error, _}, RuntimeError.exception("user required")) # raises exception |
| 49 | + ``` |
| 50 | + |
| 51 | + ### Transforming Values |
| 52 | + |
| 53 | + ```elixir |
| 54 | + # Transform success values |
| 55 | + {:ok, 5} |> Err.map(fn x -> x * 2 end) # => {:ok, 10} |
| 56 | + |
| 57 | + # Chain operations (value is extracted and passed to function) |
| 58 | + {:ok, user} |
| 59 | + |> Err.and_then(fn user -> fetch_permissions(user) end) |
| 60 | + |> Err.and_then(fn perms -> validate_access(perms) end) |
| 61 | + |
| 62 | + # Transform errors |
| 63 | + {:error, :timeout} |
| 64 | + |> Err.map_err(fn _ -> :network_error end) # => {:error, :network_error} |
| 65 | + ``` |
| 66 | + |
| 67 | + ### Working with Lists |
| 68 | + |
| 69 | + ```elixir |
| 70 | + # Combine results (fail-fast) |
| 71 | + Err.all([{:ok, 1}, {:ok, 2}]) # => {:ok, [1, 2]} |
| 72 | + Err.all([{:ok, 1}, {:error, :x}]) # => {:error, :x} |
| 73 | + |
| 74 | + # Extract only success values |
| 75 | + Err.values([{:ok, 1}, {:error, :x}, {:ok, 2}]) # => [1, 2] |
| 76 | + |
| 77 | + # Partition into successes and failures |
| 78 | + Err.partition([{:ok, 1}, {:error, "a"}, {:ok, 2}]) # => {[1, 2], ["a"]} |
| 79 | + ``` |
| 80 | + |
| 81 | + ### Guards for Pattern Matching |
| 82 | + |
| 83 | + ```elixir |
| 84 | + import Err |
| 85 | + |
| 86 | + def process(result) when is_ok(result), do: # handle success |
| 87 | + def process(result) when is_err(result), do: # handle error |
| 88 | + def process(value) when is_some(value), do: # handle non-nil |
| 89 | + ``` |
| 90 | + |
| 91 | + ## Multi-Element Tuples |
| 92 | + |
| 93 | + Err handles tuples with metadata by extracting/returning values as lists: |
| 94 | + |
| 95 | + ```elixir |
| 96 | + # Two-element tuple returns the value directly |
| 97 | + Err.unwrap_or({:ok, user}, nil) # => user |
| 98 | + |
| 99 | + # Multi-element tuple returns remaining elements as list |
| 100 | + Err.unwrap_or({:ok, user, :cached}, nil) # => [user, :cached] |
| 101 | + |
| 102 | + # Works consistently across all functions |
| 103 | + Err.map({:ok, x, y}, fn [val, meta] -> val * 2 end) |
| 104 | + ``` |
| 105 | + |
| 106 | + ## Best Practices |
| 107 | + |
| 108 | + ### 1. Chain Operations Instead of Nesting |
| 109 | + |
| 110 | + **Good:** |
| 111 | + ```elixir |
| 112 | + {:ok, input} |
| 113 | + |> Err.and_then(&validate/1) |
| 114 | + |> Err.and_then(&transform/1) |
| 115 | + |> Err.and_then(&save/1) |
| 116 | + ``` |
| 117 | + |
| 118 | + **Avoid:** |
| 119 | + ```elixir |
| 120 | + case validate(input) do |
| 121 | + {:ok, valid} -> |
| 122 | + case transform(valid) do |
| 123 | + {:ok, transformed} -> save(transformed) |
| 124 | + error -> error |
| 125 | + end |
| 126 | + error -> error |
| 127 | + end |
| 128 | + ``` |
| 129 | + |
| 130 | + ### 2. Use Guards for Cleaner Code |
| 131 | + |
| 132 | + **Good:** |
| 133 | + ```elixir |
| 134 | + def handle(result) when is_ok(result), do: extract_value(result) |
| 135 | + def handle(result) when is_err(result), do: handle_error(result) |
| 136 | + ``` |
| 137 | + |
| 138 | + **Avoid:** |
| 139 | + ```elixir |
| 140 | + def handle({:ok, _} = result), do: extract_value(result) |
| 141 | + def handle({:error, _} = result), do: handle_error(result) |
| 142 | + ``` |
| 143 | + |
| 144 | + ### 3. Prefer `unwrap_or` for Safe Defaults |
| 145 | + |
| 146 | + **Good:** |
| 147 | + ```elixir |
| 148 | + config = Err.unwrap_or(fetch_config(), default_config()) |
| 149 | + ``` |
| 150 | + |
| 151 | + **Avoid:** |
| 152 | + ```elixir |
| 153 | + config = case fetch_config() do |
| 154 | + {:ok, c} -> c |
| 155 | + {:error, _} -> default_config() |
| 156 | + end |
| 157 | + ``` |
| 158 | + |
| 159 | + ### 4. Use `expect!` Only When Value is Expected |
| 160 | + |
| 161 | + ```elixir |
| 162 | + user = Err.expect!(fetch_current_user(), RuntimeError.exception("user must be authenticated")) |
| 163 | + ``` |
| 164 | + |
| 165 | + ### 5. Leverage `or_else` for Fallbacks |
| 166 | + |
| 167 | + ```elixir |
| 168 | + # Try cache first, fall back to database |
| 169 | + get_from_cache(key) |
| 170 | + |> Err.or_else(get_from_database(key)) |
| 171 | + ``` |
| 172 | + |
| 173 | + ### 6. Use Type-Specific Functions |
| 174 | + |
| 175 | + ```elixir |
| 176 | + # For Results: map/2, map_err/2, and_then/2 |
| 177 | + # For Options: map/2 (treats nil as None) |
| 178 | + # For Lists: all/1, values/1, partition/1 |
| 179 | + ``` |
| 180 | + |
| 181 | + ## Common Mistakes to Avoid |
| 182 | + |
| 183 | + ### ❌ Don't ignore multi-element tuple behavior |
| 184 | + |
| 185 | + ```elixir |
| 186 | + # Wrong - expecting a single value |
| 187 | + {:ok, user, :cached} |> Err.map(fn u -> u.name end) # Error! u is a list |
| 188 | + |
| 189 | + # Right - handle list for multi-element tuples |
| 190 | + {:ok, user, :cached} |> Err.map(fn [u, _meta] -> u.name end) |
| 191 | + ``` |
| 192 | + |
| 193 | + ### ❌ Don't use Err.ok/error unnecessarily |
| 194 | + |
| 195 | + ```elixir |
| 196 | + # Wrong - already a result tuple |
| 197 | + result = {:ok, value} |
| 198 | + wrapped = Err.ok(result) # => {:ok, {:ok, value}} |
| 199 | + |
| 200 | + # Right - use as-is or flatten if nested |
| 201 | + result = {:ok, value} # Good |
| 202 | + Err.flatten({:ok, {:ok, value}}) # If you have nesting |
| 203 | + ``` |
| 204 | + |
| 205 | + ### ❌ Don't mix exception-based and Result-based error handling |
| 206 | + |
| 207 | + ```elixir |
| 208 | + # Wrong - mixing paradigms |
| 209 | + def process(input) do |
| 210 | + case validate(input) do |
| 211 | + {:ok, valid} -> |
| 212 | + try do |
| 213 | + transform(valid) # May raise |
| 214 | + rescue |
| 215 | + e -> {:error, e} |
| 216 | + end |
| 217 | + error -> error |
| 218 | + end |
| 219 | + end |
| 220 | + |
| 221 | + # Right - be consistent |
| 222 | + def process(input) do |
| 223 | + {:ok, input} |
| 224 | + |> Err.and_then(&validate/1) |
| 225 | + |> Err.and_then(&transform/1) |
| 226 | + end |
| 227 | + ``` |
| 228 | + |
| 229 | + ### ❌ Don't use `and_then` when `map` is sufficient |
| 230 | + |
| 231 | + ```elixir |
| 232 | + # Wrong - unnecessary wrapping |
| 233 | + {:ok, 5} |> Err.and_then(fn x -> {:ok, x * 2} end) |
| 234 | + |
| 235 | + # Right - use map when you're just transforming |
| 236 | + {:ok, 5} |> Err.map(fn x -> x * 2 end) |
| 237 | + ``` |
| 238 | + |
| 239 | + ## Function Quick Reference |
| 240 | + |
| 241 | + | Function | Purpose | Example | |
| 242 | + |----------|---------|---------| |
| 243 | + | `ok/1`, `error/1` | Wrap values | `Err.ok(user)` | |
| 244 | + | `unwrap_or/2` | Extract value with default | `unwrap_or({:ok, x}, 0)` | |
| 245 | + | `expect!/2` | Extract or raise | `expect!({:ok, x}, Error.exception("msg"))` | |
| 246 | + | `map/2` | Transform success value | `map({:ok, x}, &(&1 * 2))` | |
| 247 | + | `map_err/2` | Transform error | `map_err({:error, x}, &to_string/1)` | |
| 248 | + | `and_then/2` | Chain operations | `and_then({:ok, x}, &process/1)` | |
| 249 | + | `all/1` | Combine results (fail-fast) | `all([{:ok, 1}, {:ok, 2}])` | |
| 250 | + | `values/1` | Extract success values | `values([{:ok, 1}, {:error, 2}])` | |
| 251 | + | `partition/1` | Split into ok/error lists | `partition([{:ok, 1}, {:error, 2}])` | |
| 252 | + | `is_ok/1`, `is_err/1` | Guards for pattern matching | `when is_ok(result)` | |
| 253 | + | `flatten/1` | Flatten nested results | `flatten({:ok, {:ok, x}})` | |
| 254 | + |
| 255 | + ## Summary |
| 256 | + |
| 257 | + Err provides functional error handling for Elixir. Use it to make error states explicit, chain operations safely, and avoid exception-based control flow. The library shines when building composable domain logic with clear success/failure paths. |