Skip to content

Commit ff77f5c

Browse files
committed
chore: add docs
1 parent ad2beb2 commit ff77f5c

File tree

10 files changed

+1643
-2
lines changed

10 files changed

+1643
-2
lines changed
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Error Design Principles
2+
3+
This document explains the design philosophy and principles behind the ErrorMessage library, helping you understand why it was created and how it can improve your application's error handling.
4+
5+
## Why Consistent Error Handling Matters
6+
7+
Error handling is a critical aspect of any robust application. Inconsistent error handling can lead to:
8+
9+
1. **Unpredictable behavior**: When errors are represented differently across your application, it becomes difficult to predict how errors will be structured.
10+
2. **Fragile error handling**: String-based error matching is brittle and can break when error messages change.
11+
3. **Poor user experience**: Inconsistent error reporting leads to confusing user interfaces and API responses.
12+
4. **Difficult debugging**: Without structured errors, it's harder to track down the root cause of issues.
13+
14+
ErrorMessage addresses these problems by providing a consistent structure for all errors in your application.
15+
16+
## HTTP Status Codes as a Universal Language
17+
18+
ErrorMessage uses HTTP status codes as the foundation for error classification. This approach has several advantages:
19+
20+
1. **Universality**: HTTP status codes are widely understood across programming languages and platforms.
21+
2. **Semantic meaning**: Each status code has a well-defined meaning (e.g., 404 for "not found", 403 for "forbidden").
22+
3. **Standardization**: Using HTTP status codes means your error system aligns with web standards.
23+
4. **API compatibility**: When building web APIs, your internal error representation maps directly to HTTP responses.
24+
25+
Even for non-web applications, HTTP status codes provide a comprehensive and well-understood taxonomy of error conditions that can be applied to many domains.
26+
27+
## The Three-Part Error Structure
28+
29+
ErrorMessage uses a three-part structure for errors:
30+
31+
1. **Code**: An atom representing the error type (e.g., `:not_found`, `:internal_server_error`)
32+
2. **Message**: A human-readable description of what went wrong
33+
3. **Details**: Additional context-specific information about the error
34+
35+
This structure provides a balance between:
36+
37+
- **Simplicity**: The core structure is easy to understand and use
38+
- **Flexibility**: The details field can contain any data structure needed for your specific use case
39+
- **Consistency**: All errors follow the same pattern, making them predictable
40+
41+
## Pattern Matching vs. String Matching
42+
43+
One of the key benefits of ErrorMessage is the ability to pattern match on error codes rather than error messages:
44+
45+
```elixir
46+
# Fragile approach (depends on exact message text)
47+
case result do
48+
{:error, "User not found: " <> _} -> handle_not_found()
49+
# ...
50+
end
51+
52+
# Robust approach with ErrorMessage
53+
case result do
54+
{:error, %ErrorMessage{code: :not_found}} -> handle_not_found()
55+
# ...
56+
end
57+
```
58+
59+
Pattern matching on error codes is:
60+
61+
1. **More robust**: Changing error messages doesn't break your error handling logic
62+
2. **More explicit**: The intent of your code is clearer
63+
3. **More maintainable**: Error handling code is less likely to break over time
64+
65+
## Serialization Considerations
66+
67+
ErrorMessage includes built-in support for converting errors to strings (for logging) and maps (for JSON serialization). This addresses common challenges with error serialization:
68+
69+
1. **Complex data structures**: The `ensure_json_serializable/1` function handles conversion of Elixir-specific data types (like PIDs, Date/Time structs, etc.) to JSON-compatible formats.
70+
2. **Request context**: When available, the request ID is automatically included in serialized errors, making it easier to correlate errors with specific requests.
71+
3. **Consistent output**: All errors are serialized in a consistent format, making it easier to process them in clients.
72+
73+
## Integration with Existing Systems
74+
75+
ErrorMessage is designed to integrate seamlessly with:
76+
77+
- **Phoenix**: For web API error responses
78+
- **Logger**: For structured error logging
79+
- **Plug**: For HTTP status code mapping
80+
81+
This integration-friendly design means you can adopt ErrorMessage incrementally in your application without having to rewrite existing code all at once.
82+
83+
## When to Use ErrorMessage
84+
85+
ErrorMessage is particularly valuable in:
86+
87+
1. **API-driven applications**: Where consistent error responses are crucial
88+
2. **Microservice architectures**: Where errors might cross service boundaries
89+
3. **Large codebases**: Where consistency across modules is important
90+
4. **Long-lived applications**: Where maintainability over time is a priority
91+
92+
While ErrorMessage can be used in any Elixir application, these scenarios highlight where it provides the most value.
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
# Error Handling in Elixir and ErrorMessage
2+
3+
This document explains how error handling typically works in Elixir and how ErrorMessage enhances the standard approach.
4+
5+
## Traditional Error Handling in Elixir
6+
7+
Elixir, like Erlang, follows the "Let it crash" philosophy, which encourages developers to focus on the happy path and let supervisors handle failures. However, this doesn't mean that we should ignore errors entirely. For expected errors that are part of normal program flow, Elixir provides several conventions:
8+
9+
### Return Values
10+
11+
The most common pattern in Elixir for handling errors is to return tagged tuples:
12+
13+
```elixir
14+
# Success case
15+
{:ok, result}
16+
17+
# Error case
18+
{:error, reason}
19+
```
20+
21+
This pattern allows for easy pattern matching:
22+
23+
```elixir
24+
case MyModule.some_function() do
25+
{:ok, result} -> handle_success(result)
26+
{:error, reason} -> handle_error(reason)
27+
end
28+
```
29+
30+
### Exceptions
31+
32+
Elixir also supports exceptions for exceptional circumstances:
33+
34+
```elixir
35+
try do
36+
some_function_that_might_raise()
37+
rescue
38+
e in SomeError -> handle_error(e)
39+
end
40+
```
41+
42+
However, exceptions are generally used for unexpected errors rather than as a control flow mechanism.
43+
44+
## Limitations of Traditional Approaches
45+
46+
While these approaches work well for simple cases, they have limitations:
47+
48+
1. **Inconsistent Error Structure**: The `:error` tuple can contain any term as its reason, leading to inconsistent error handling across different parts of an application.
49+
50+
2. **String-Based Error Messages**: Many libraries use simple strings as error reasons, which makes pattern matching fragile.
51+
52+
3. **Limited Context**: Error reasons often lack detailed context about what went wrong.
53+
54+
4. **No Standard for HTTP Integration**: When building web applications, there's no standard way to map errors to HTTP status codes.
55+
56+
## How ErrorMessage Improves Error Handling
57+
58+
ErrorMessage addresses these limitations by providing:
59+
60+
### 1. Consistent Structure
61+
62+
All errors follow the same structure:
63+
64+
```elixir
65+
%ErrorMessage{
66+
code: :error_code,
67+
message: "Human-readable message",
68+
details: additional_context
69+
}
70+
```
71+
72+
This consistency makes error handling more predictable across your application.
73+
74+
### 2. Code-Based Pattern Matching
75+
76+
Instead of matching on error messages (which might change), you can match on error codes:
77+
78+
```elixir
79+
case result do
80+
{:ok, value} -> handle_success(value)
81+
{:error, %ErrorMessage{code: :not_found}} -> handle_not_found()
82+
{:error, %ErrorMessage{code: :unauthorized}} -> handle_unauthorized()
83+
{:error, _} -> handle_other_errors()
84+
end
85+
```
86+
87+
### 3. Rich Context
88+
89+
The `details` field can contain any data structure, allowing you to provide rich context about what went wrong:
90+
91+
```elixir
92+
ErrorMessage.not_found("User not found", %{
93+
user_id: id,
94+
search_params: params,
95+
timestamp: DateTime.utc_now()
96+
})
97+
```
98+
99+
### 4. HTTP Integration
100+
101+
ErrorMessage is built around HTTP status codes, making it easy to integrate with web applications:
102+
103+
```elixir
104+
def render_error(conn, %ErrorMessage{} = error) do
105+
conn
106+
|> put_status(ErrorMessage.http_code(error))
107+
|> json(ErrorMessage.to_jsonable_map(error))
108+
end
109+
```
110+
111+
## ErrorMessage in the Elixir Ecosystem
112+
113+
ErrorMessage complements other Elixir error handling approaches:
114+
115+
### With Standard Libraries
116+
117+
ErrorMessage works alongside standard Elixir patterns:
118+
119+
```elixir
120+
def find_user(id) do
121+
case Repo.get(User, id) do
122+
nil -> {:error, ErrorMessage.not_found("User not found", %{user_id: id})}
123+
user -> {:ok, user}
124+
end
125+
end
126+
```
127+
128+
### With Ecto
129+
130+
You can convert Ecto changeset errors to ErrorMessage:
131+
132+
```elixir
133+
def create_user(params) do
134+
%User{}
135+
|> User.changeset(params)
136+
|> Repo.insert()
137+
|> case do
138+
{:ok, user} ->
139+
{:ok, user}
140+
{:error, changeset} ->
141+
errors = Ecto.Changeset.traverse_errors(changeset, &translate_error/1)
142+
{:error, ErrorMessage.unprocessable_entity("Invalid user data", errors)}
143+
end
144+
end
145+
```
146+
147+
### With Phoenix
148+
149+
ErrorMessage integrates well with Phoenix for API error handling:
150+
151+
```elixir
152+
defmodule MyApp.FallbackController do
153+
use Phoenix.Controller
154+
155+
def call(conn, {:error, %ErrorMessage{} = error}) do
156+
conn
157+
|> put_status(ErrorMessage.http_code(error))
158+
|> put_view(MyApp.ErrorView)
159+
|> render("error.json", error: error)
160+
end
161+
end
162+
```
163+
164+
## When to Use ErrorMessage
165+
166+
ErrorMessage is particularly valuable in:
167+
168+
1. **API-driven applications**: Where consistent error responses are crucial
169+
2. **Complex business logic**: Where detailed error context helps with debugging
170+
3. **Cross-module boundaries**: Where consistent error handling improves maintainability
171+
4. **Web applications**: Where mapping errors to HTTP status codes is important
172+
173+
Most systems will benefit from ErrorMessage providing significant benefits for applications that need structured, consistent error handling, as well as benefitting pattern matching on error codes.
174+
175+
For further reading checkout this [blog post](https://learn-elixir.dev/blogs/safer-error-systems-in-elixir)

0 commit comments

Comments
 (0)