docs/how-to-guides/error_handling_patterns.md

# Common Error Handling Patterns

This guide demonstrates common patterns for handling errors with the ErrorMessage library in different contexts.

## Pattern Matching on Error Codes

One of the most powerful features of ErrorMessage is the ability to pattern match on error codes:

```elixir
def process_result(result) do
  # Generally we won't be returning atoms through our
  # system but as an example of how we can handle them

  case result do
    {:ok, value} ->
      # Handle success case
      {:ok, process_value(value)}
      
    {:error, %ErrorMessage{code: :not_found}} ->
      # Handle not found specifically
      {:error, :resource_missing}
      
    {:error, %ErrorMessage{code: :unauthorized}} ->
      # Handle unauthorized specifically
      {:error, :permission_denied}
      
    {:error, %ErrorMessage{} = error} ->
      # Handle any other error
      Logger.error("Unexpected error: #{error}")
      {:error, :unexpected_error}
  end
end
```

## Using with/1 for Error Handling

The `with/1` special form in Elixir works great with ErrorMessage for handling multiple operations that might fail:

```elixir
def create_order(user_id, product_id, quantity) do
  with {:ok, user} <- find_user(user_id),
       {:ok, product} <- find_product(product_id),
       {:ok, _} <- check_inventory(product, quantity),
       {:ok, order} <- create_order_record(user, product, quantity) do
    {:ok, order}
  else
    {:error, %ErrorMessage{code: :not_found, details: %{user_id: _}}} ->
      {:error, ErrorMessage.bad_request("Invalid user", %{user_id: user_id})}
      
    {:error, %ErrorMessage{code: :not_found, details: %{product_id: _}}} ->
      {:error, ErrorMessage.bad_request("Invalid product", %{product_id: product_id})}
      
    {:error, error} ->
      # Pass through any other errors
      {:error, error}
  end
end
```

## Converting Other Error Types to ErrorMessage

Often you'll need to convert errors from other libraries to ErrorMessage format:

```elixir
def handle_ecto_errors(changeset) do
  errors = Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
    Enum.reduce(opts, msg, fn {key, value}, acc ->
      String.replace(acc, "%{#{key}}", to_string(value))
    end)
  end)
  
  ErrorMessage.unprocessable_entity("Validation failed", errors)
end

def handle_file_errors(file_path) do
  case File.read(file_path) do
    {:ok, content} ->
      {:ok, content}
      
    {:error, :enoent} ->
      {:error, ErrorMessage.not_found("File not found", %{path: file_path})}
      
    {:error, :eacces} ->
      {:error, ErrorMessage.forbidden("Permission denied", %{path: file_path})}
      
    {:error, reason} ->
      {:error, ErrorMessage.internal_server_error("File error", %{reason: reason, path: file_path})}
  end
end
```

## Error Handling in GenServers

ErrorMessage works well in GenServer implementations:

```elixir
defmodule MyApp.UserManager do
  use GenServer
  
  # Client API
  
  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts, name: __MODULE__)
  end
  
  def get_user(id) do
    GenServer.call(__MODULE__, {:get_user, id})
  end
  
  # Server Callbacks
  
  def init(opts) do
    {:ok, %{users: opts[:initial_users] || %{}}}
  end
  
  def handle_call({:get_user, id}, _from, state) do
    case Map.get(state.users, id) do
      nil ->
        {:reply, {:error, ErrorMessage.not_found("User not found", %{user_id: id})}, state}
        
      user ->
        {:reply, {:ok, user}, state}
    end
  end
end
```

## Composing Error Handlers

You can create helper functions to handle common error patterns:

```elixir
defmodule MyApp.ErrorHelpers do
  require Logger
  
  def with_logging(result, context) do
    case result do
      {:ok, _} = success ->
        success
        
      {:error, %ErrorMessage{} = error} ->
        Logger.error("[#{context}] #{error}")
        {:error, error}
    end
  end
  
  def with_fallback(result, fallback_fn) do
    case result do
      {:ok, _} = success ->
        success
        
      {:error, %ErrorMessage{code: :not_found}} ->
        fallback_fn.()
        
      {:error, _} = error ->
        error
    end
  end
  
  def with_retry(operation, retry_count \\ 3) do
    retry_with_count(operation, retry_count)
  end
  
  defp retry_with_count(operation, count) when count > 0 do
    case operation.() do
      {:ok, _} = success ->
        success
        
      {:error, %ErrorMessage{code: code} = error} when code in [:service_unavailable, :gateway_timeout] ->
        # Only retry on certain errors
        Process.sleep(100)
        retry_with_count(operation, count - 1)
        
      {:error, _} = error ->
        error
    end
  end
  
  defp retry_with_count(_operation, 0) do
    {:error, ErrorMessage.service_unavailable("Operation failed after multiple retries")}
  end
end
```

Usage:

```elixir
import MyApp.ErrorHelpers

def process_user(id) do
  with_logging(
    with_fallback(
      UserRepository.find_user(id),
      fn -> UserRepository.create_default_user(id) end
    ),
    "UserProcessing"
  )
end

def fetch_remote_data(url) do
  with_retry(fn -> ApiClient.get(url) end)
end
```

## Handling Errors in Concurrent Operations

When working with concurrent operations, you can collect and process errors:

```elixir
def process_items(items) do
  items
  |> Enum.map(fn item ->
    Task.async(fn -> process_item(item) end)
  end)
  |> Task.await_many()
  |> Enum.split_with(fn
    {:ok, _} -> true
    {:error, _} -> false
  end)
  |> case do
    {successes, []} ->
      # All operations succeeded
      {:ok, Enum.map(successes, fn {:ok, result} -> result end)}
      
    {_, errors} ->
      # Some operations failed
      error_details = Enum.map(errors, fn {:error, error} -> 
        %{item_id: error.details.item_id, reason: error.message}
      end)
      
      {:error, ErrorMessage.multi_status("Some operations failed", %{errors: error_details})}
  end
end
```

These patterns should help you effectively use ErrorMessage in various scenarios throughout your application.