Packages

Tiny library for composing and normalizing Elixir error flows with existing {:ok, value}, {:error, reason}, and nil conventions.

Current section

5 Versions

Jump to

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.