Raxol.Core.ErrorHandling (Raxol Core v2.4.0)

Copy Markdown View Source

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

error_context()

@type error_context() :: map()

error_result()

@type error_result() ::
  {:error, error_type(), String.t()}
  | {:error, error_type(), String.t(), error_context()}

error_severity()

@type error_severity() :: :debug | :info | :warning | :error | :critical

error_type()

@type error_type() ::
  :validation
  | :runtime
  | :system
  | :network
  | :permission
  | :not_found
  | :timeout

result(ok)

@type result(ok) :: {:ok, ok} | {:error, term()}

result(ok, error)

@type result(ok, error) :: {:ok, ok} | {:error, error}

Functions

ensure_cleanup(main_fun, cleanup_fun)

@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.

error(type, message, context \\ %{})

Creates a standardized error tuple.

Examples

error(:validation, "Invalid email format")
error(:not_found, "User not found", %{user_id: 123})

execute_pipeline(steps)

Chains multiple {:step, name, fun} tuples, halting on first error.

Examples

steps = [
  {:step, :validate, &validate/1},
  {:step, :process, &process/1}
]
execute_pipeline(steps)

execute_with_handling(operation, opts, fun)

Executes a function with error handling, retry, and telemetry.

flat_map(error, fun)

@spec flat_map(result(a), (a -> result(b))) :: result(b) when a: any(), b: any()

FlatMaps over a Result type.

Examples

{:ok, 5}
|> flat_map(fn x -> {:ok, x * 2} end)
# => {:ok, 10}

handle_error(error, opts)

Handles an error result with optional recovery.

Examples

result
|> handle_error(default: "fallback")
|> handle_error(with: fn error -> recover(error) end)

handle_genserver_error(error, state, module)

Creates a supervisor-friendly error handler for GenServer processes.

log_error(operation, error, context \\ %{}, severity \\ :error)

Logs an error with operation context and severity.

map(error, fun)

@spec map(result(a), (a -> b)) :: result(b) when a: any(), b: any()

Maps over a Result type.

Examples

{:ok, 5}
|> map(fn x -> x * 2 end)
# => {:ok, 10}

normalize_error(error)

Converts various error formats to a standardized {:error, type, message, context} tuple.

safe_apply(module, function, args)

@spec safe_apply(module(), atom(), list()) :: {:ok, any()} | {:error, atom()}

Safely calls a module function if it's exported.

Examples

safe_apply(MyModule, :init, [])

safe_arithmetic(fun, value, fallback \\ 0)

@spec safe_arithmetic((number() -> number()), any(), number()) :: number()

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)

safe_batch(functions)

@spec safe_batch([(-> any())]) :: [result(any())]

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}]

safe_call(fun)

@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"}}

safe_call_with_default(fun, default)

@spec safe_call_with_default((-> any()), any()) :: any()

Safely executes a function with a fallback value on error.

Examples

iex> safe_call_with_default(fn -> raise "oops" end, 42)
42

safe_call_with_info(fun)

@spec safe_call_with_info((-> any())) ::
  {:ok, any()} | {:error, {atom(), any(), list()}}

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"}, [...]}}

safe_call_with_logging(fun, context)

@spec safe_call_with_logging((-> any()), String.t()) :: result(any())

Safely executes a function with error logging.

Examples

safe_call_with_logging(fn -> process() end, "Processing failed")

safe_callback(module, function, args)

@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.

safe_deserialize(binary)

@spec safe_deserialize(binary()) :: {:ok, term()} | {:error, :invalid_binary}

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}

safe_genserver_call(server, message, timeout \\ 5000)

@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)

safe_read_term(path)

@spec safe_read_term(Path.t()) :: {:ok, term()} | {:error, atom()}

Safely reads and deserializes a file.

Examples

safe_read_term("/path/to/file")

safe_sequence(functions)

@spec safe_sequence([(-> any())]) :: result([any()])

Executes operations until one fails.

safe_serialize(term)

@spec safe_serialize(term()) :: result(binary())

Safely serializes a term to binary.

safe_write_term(path, term)

@spec safe_write_term(Path.t(), term()) :: result(:ok)

Safely writes a term to a file.

unwrap_or(arg, default)

@spec unwrap_or(result(a), a) :: a when a: any()

Unwraps a Result or returns a default value.

Examples

unwrap_or({:ok, 42}, 0)     # => 42
unwrap_or({:error, _}, 0)   # => 0

unwrap_or_else(arg, fun)

@spec unwrap_or_else(result(a), (-> a)) :: a when a: any()

Unwraps a Result or calls a function to get default.

Examples

unwrap_or_else({:error, :not_found}, fn -> fetch_default() end)

with_cleanup(main_fun, cleanup_fun)

@spec with_cleanup((-> result(a)), (a -> any())) :: result(a) when a: any()

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
)

with_error_handling(operation, opts \\ [], list)

(macro)

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