Common Error Handling Patterns
View SourceThis 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:
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:
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:
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:
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:
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:
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:
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.