Functional error handling patterns for Raxol.
This module provides composable error handling utilities to replace try/catch blocks with more functional patterns. It implements Result and Option types along with safe execution functions.
Philosophy
Instead of using try/catch for error handling, we use:
Result types ({:ok, value} | {:error, reason})
- Safe execution wrappers
- Pipeline-friendly error handling
- Explicit error propagation
Examples
# Instead of try/catch
result = safe_call(fn -> risky_operation() end)
# Chain operations safely
with {:ok, data} <- fetch_data(),
{:ok, processed} <- process_data(data),
{:ok, result} <- save_result(processed) do
{:ok, result}
end
# Safe binary operations
safe_deserialize(binary_data)
Summary
Functions
Ensures cleanup is called regardless of success or failure.
Creates a standardized error tuple.
Chains multiple {:step, name, fun} tuples, halting on first error.
Executes a function with error handling, retry, and telemetry.
FlatMaps over a Result type.
Handles an error result with optional recovery.
Creates a supervisor-friendly error handler for GenServer processes.
Logs an error with operation context and severity.
Maps over a Result type.
Converts various error formats to a standardized {:error, type, message, context} tuple.
Safely calls a module function if it's exported.
Safely performs arithmetic with a fallback for nil values.
Safely executes multiple operations, collecting all results.
Safely executes a function and returns a Result type.
Safely executes a function with a fallback value on error.
Safely executes a function and returns error details with stacktrace for re-raising.
Safely executes a function with error logging.
Safely calls an optional callback on a module. Returns {:ok, nil} if the callback doesn't exist.
Safely deserializes Erlang terms from binary data.
Safely makes a GenServer call with proper error handling.
Safely reads and deserializes a file.
Executes operations until one fails.
Safely serializes a term to binary.
Safely writes a term to a file.
Unwraps a Result or returns a default value.
Unwraps a Result or calls a function to get default.
Ensures a cleanup function is called even if the main function fails.
Wraps a block with error handling, logging, and optional retry.
Types
@type error_context() :: map()
@type error_result() :: {:error, error_type(), String.t()} | {:error, error_type(), String.t(), error_context()}
@type error_severity() :: :debug | :info | :warning | :error | :critical
@type error_type() ::
:validation
| :runtime
| :system
| :network
| :permission
| :not_found
| :timeout
@type result(ok) :: {:ok, ok} | {:error, term()}
@type result(ok, error) :: {:ok, ok} | {:error, error}
Functions
@spec ensure_cleanup((-> any()), (-> any())) :: {:ok, any()} | {:error, Exception.t() | {:exit, term()} | {:throw, term()} | {atom(), term()}}
Ensures cleanup is called regardless of success or failure.
Creates a standardized error tuple.
Examples
error(:validation, "Invalid email format")
error(:not_found, "User not found", %{user_id: 123})
Chains multiple {:step, name, fun} tuples, halting on first error.
Examples
steps = [
{:step, :validate, &validate/1},
{:step, :process, &process/1}
]
execute_pipeline(steps)
Executes a function with error handling, retry, and telemetry.
FlatMaps over a Result type.
Examples
{:ok, 5}
|> flat_map(fn x -> {:ok, x * 2} end)
# => {:ok, 10}
Handles an error result with optional recovery.
Examples
result
|> handle_error(default: "fallback")
|> handle_error(with: fn error -> recover(error) end)
Creates a supervisor-friendly error handler for GenServer processes.
Logs an error with operation context and severity.
Maps over a Result type.
Examples
{:ok, 5}
|> map(fn x -> x * 2 end)
# => {:ok, 10}
Converts various error formats to a standardized {:error, type, message, context} tuple.
Safely calls a module function if it's exported.
Examples
safe_apply(MyModule, :init, [])
Safely performs arithmetic with a fallback for nil values.
Examples
safe_arithmetic(fn x -> x + 10 end, nil, 0)
# => 10 (uses fallback 0, then adds 10)
Safely executes multiple operations, collecting all results.
Examples
safe_batch([
fn -> operation1() end,
fn -> operation2() end,
fn -> operation3() end
])
# => [{:ok, result1}, {:error, error2}, {:ok, result3}]
@spec safe_call((-> any())) :: {:ok, any()} | {:error, Exception.t() | {:exit, term()} | {:throw, term()} | {atom(), term()}}
Safely executes a function and returns a Result type.
Examples
iex> safe_call(fn -> 1 + 1 end)
{:ok, 2}
iex> safe_call(fn -> raise "oops" end)
{:error, %RuntimeError{message: "oops"}}
Safely executes a function with a fallback value on error.
Examples
iex> safe_call_with_default(fn -> raise "oops" end, 42)
42
Safely executes a function and returns error details with stacktrace for re-raising.
Examples
iex> safe_call_with_info(fn -> 42 end)
{:ok, 42}
iex> safe_call_with_info(fn -> raise "oops" end)
{:error, {:error, %RuntimeError{message: "oops"}, [...]}}
Safely executes a function with error logging.
Examples
safe_call_with_logging(fn -> process() end, "Processing failed")
@spec safe_callback(module(), atom(), list()) :: {:ok, any()} | {:error, Exception.t() | {:exit, term()} | {:throw, term()} | {atom(), term()}}
Safely calls an optional callback on a module. Returns {:ok, nil} if the callback doesn't exist.
Safely deserializes Erlang terms from binary data.
Examples
iex> binary = :erlang.term_to_binary({:ok, "data"})
iex> safe_deserialize(binary)
{:ok, {:ok, "data"}}
iex> safe_deserialize("invalid")
{:error, :invalid_binary}
@spec safe_genserver_call(GenServer.server(), any(), timeout()) :: result(any())
Safely makes a GenServer call with proper error handling.
Examples
safe_genserver_call(MyServer, :get_state)
Safely reads and deserializes a file.
Examples
safe_read_term("/path/to/file")
Executes operations until one fails.
Safely serializes a term to binary.
Safely writes a term to a file.
Unwraps a Result or returns a default value.
Examples
unwrap_or({:ok, 42}, 0) # => 42
unwrap_or({:error, _}, 0) # => 0
Unwraps a Result or calls a function to get default.
Examples
unwrap_or_else({:error, :not_found}, fn -> fetch_default() end)
Ensures a cleanup function is called even if the main function fails.
Examples
with_cleanup(
fn -> open_resource() end,
fn resource -> close_resource(resource) end
)
Wraps a block with error handling, logging, and optional retry.
Options
:context- Additional context map to log with errors:severity- Error severity level (default: :error):fallback- Fallback value on error:retry- Number of retry attempts (default: 0):retry_delay- Delay between retries in ms (default: 1000)
Examples
import Raxol.Core.ErrorHandling
with_error_handling(:database_query, context: %{user_id: 123}) do
Repo.get!(User, user_id)
end