docs/explanation/error_handling_in_elixir.md

# Error Handling in Elixir and ErrorMessage

This document explains how error handling typically works in Elixir and how ErrorMessage enhances the standard approach.

## Traditional Error Handling in Elixir

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:

### Return Values

The most common pattern in Elixir for handling errors is to return tagged tuples:

```elixir
# Success case
{:ok, result}

# Error case
{:error, reason}
```

This pattern allows for easy pattern matching:

```elixir
case MyModule.some_function() do
  {:ok, result} -> handle_success(result)
  {:error, reason} -> handle_error(reason)
end
```

### Exceptions

Elixir also supports exceptions for exceptional circumstances:

```elixir
try do
  some_function_that_might_raise()
rescue
  e in SomeError -> handle_error(e)
end
```

However, exceptions are generally used for unexpected errors rather than as a control flow mechanism.

## Limitations of Traditional Approaches

While these approaches work well for simple cases, they have limitations:

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.

2. **String-Based Error Messages**: Many libraries use simple strings as error reasons, which makes pattern matching fragile.

3. **Limited Context**: Error reasons often lack detailed context about what went wrong.

4. **No Standard for HTTP Integration**: When building web applications, there's no standard way to map errors to HTTP status codes.

## How ErrorMessage Improves Error Handling

ErrorMessage addresses these limitations by providing:

### 1. Consistent Structure

All errors follow the same structure:

```elixir
%ErrorMessage{
  code: :error_code,
  message: "Human-readable message",
  details: additional_context
}
```

This consistency makes error handling more predictable across your application.

### 2. Code-Based Pattern Matching

Instead of matching on error messages (which might change), you can match on error codes:

```elixir
case result do
  {:ok, value} -> handle_success(value)
  {:error, %ErrorMessage{code: :not_found}} -> handle_not_found()
  {:error, %ErrorMessage{code: :unauthorized}} -> handle_unauthorized()
  {:error, _} -> handle_other_errors()
end
```

### 3. Rich Context

The `details` field can contain any data structure, allowing you to provide rich context about what went wrong:

```elixir
ErrorMessage.not_found("User not found", %{
  user_id: id,
  search_params: params,
  timestamp: DateTime.utc_now()
})
```

### 4. HTTP Integration

ErrorMessage is built around HTTP status codes, making it easy to integrate with web applications:

```elixir
def render_error(conn, %ErrorMessage{} = error) do
  conn
  |> put_status(ErrorMessage.http_code(error))
  |> json(ErrorMessage.to_jsonable_map(error))
end
```

## ErrorMessage in the Elixir Ecosystem

ErrorMessage complements other Elixir error handling approaches:

### With Standard Libraries

ErrorMessage works alongside standard Elixir patterns:

```elixir
def find_user(id) do
  case Repo.get(User, id) do
    nil -> {:error, ErrorMessage.not_found("User not found", %{user_id: id})}
    user -> {:ok, user}
  end
end
```

### With Ecto

You can convert Ecto changeset errors to ErrorMessage:

```elixir
def create_user(params) do
  %User{}
  |> User.changeset(params)
  |> Repo.insert()
  |> case do
    {:ok, user} -> 
      {:ok, user}
    {:error, changeset} ->
      errors = Ecto.Changeset.traverse_errors(changeset, &translate_error/1)
      {:error, ErrorMessage.unprocessable_entity("Invalid user data", errors)}
  end
end
```

### With Phoenix

ErrorMessage integrates well with Phoenix for API error handling:

```elixir
defmodule MyApp.FallbackController do
  use Phoenix.Controller
  
  def call(conn, {:error, %ErrorMessage{} = error}) do
    conn
    |> put_status(ErrorMessage.http_code(error))
    |> put_view(MyApp.ErrorView)
    |> render("error.json", error: error)
  end
end
```

## When to Use ErrorMessage

ErrorMessage is particularly valuable in:

1. **API-driven applications**: Where consistent error responses are crucial
2. **Complex business logic**: Where detailed error context helps with debugging
3. **Cross-module boundaries**: Where consistent error handling improves maintainability
4. **Web applications**: Where mapping errors to HTTP status codes is important

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.

For further reading checkout this [blog post](https://learn-elixir.dev/blogs/safer-error-systems-in-elixir)