Triage (triage v0.7.1)

View Source

Documentation for Triage.

Summary

Functions > Contexts

Wraps result/0 with additional context information, leaving :ok results unchanged.

Wrap errors from a result with both a context string and metadata. See wrap_context/2

Functions > Control Flow

For dealing with :error cases, passing :ok results through unchanged.

Executes a function with a previous result value, with exception handling.

Executes a function with a previous result value, without exception handling.

Executes a function that returns a result tuple, with exception handling.

Executes a function that returns a result tuple, without exception handling.

Executes a side-effect function when the result is an error, passing the result through unchanged.

Executes a side-effect function when the result is :ok, passing the result through unchanged.

Functions > Enumeration

Validates that a callback function gives an :ok / {:ok, _} result for all elements in the given enumerable.

Finds the first successful result from applying a function to enumerable elements.

Maps a function over an enumerable, collecting successful values and short-circuiting on the first error.

Functions > Helpers

Checks if a result is an error (:error or {:error, any()}).

Logs a result tuple and returns it unchanged.

Checks if a result is a success (:ok or {:ok, any()}).

Generates a user-friendly error message from various error types.

Types

result()

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

value_result()

@type value_result() :: {:ok, any()} | :error | {:error, any()}

Functions > Contexts

wrap_context(result, context)

@spec wrap_context(result(), String.t() | keyword() | map()) ::
  :ok | {:ok, any()} | {:error, Triage.WrappedError.t()}

Wraps result/0 with additional context information, leaving :ok results unchanged.

Takes a result tuple and wraps error cases (:error or {:error, reason}) with a context string, metadata, and stacktrace info contained in Triage.WrappedError{}.

If the second argument is a string, the context is set. If the second argument is a keyword list or a map the metadata is set. The arity 3 version allows setting both.

Because the context is designed to show what was happening in logs or in user error messages, the context string should describe what is being attempted in business logic terms. e.g.:

  • GOOD: "connecting to user service" (describe the action)
  • BAD: "failed to connect to user service" ("failed to connect" describes a failure)
  • BAD: "HTTP request to user_authentication server" (technical terms)

The log/2 and user_message/1 functions support WrappedError results. See their docs for more details

Examples

iex> Triage.wrap_context(:ok, "fetching user")
:ok

iex> Triage.wrap_context({:ok, 42}, "fetching user")
{:ok, 42}

iex> Triage.wrap_context(:error, "fetching user")
{:error, %Triage.WrappedError{}}

iex> Triage.wrap_context({:error, :not_found}, "fetching user", %{user_id: 123})
{:error, %Triage.WrappedError{}}

iex> Triage.wrap_context({:error, :not_found}, %{user_id: 123})
{:error, %Triage.WrappedError{}}

wrap_context(result, context, metadata)

@spec wrap_context(result(), String.t() | nil, keyword() | map()) ::
  :ok | {:ok, any()} | {:error, Triage.WrappedError.t()}

Wrap errors from a result with both a context string and metadata. See wrap_context/2

Functions > Control Flow

error_then(result, func)

@spec error_then(result(), (any() -> any())) :: result()

For dealing with :error cases, passing :ok results through unchanged.

When given result is {:error, reason}, the reason is passed into the callback function. The callback function can then return a new reason which will be returned from error_then wrapped in an {:error, _} tuple.

If :error is the given result, nil will be given to the callback function.

The callback function can also return :ok or {:ok, any()} to have the error be ignored and the :ok result will be returned instead.

Examples

iex> ping_account_server() |> Triage.error_then(fn _ -> :account_server_failure end)
{:error, :account_server_failure}

iex> Triage.error_then({:error, :unknown}, fn :unknown -> {:ok, @default_value} end)
{:ok, ...}

iex> Triage.error_then(:ok, fn _ -> :not_used end)
:ok

iex> Triage.error_then({:ok, ...}, fn _ -> :not_used end)
{:ok, 42}

iex> Triage.error_then(:error, fn nil -> :handled end)
{:error, :handled}

ok_then(result, func, opts \\ [])

@spec ok_then(result(), (any() -> any()), [{:retries, non_neg_integer()}]) :: result()

Executes a function with a previous result value, with exception handling.

Takes a result and, if :ok, passes the value from the tuple to the provided function. If the previous result was an error, short-circuits and returns the error.

If the function raises an exception, it catches it and returns {:error, %Triage.WrappedError{}} with details about the exception.

Parameters

  • result - The previous result (:ok, {:ok, value}, :error, or {:error, reason})
  • func - A function that takes the unwrapped value and returns a result

Examples

iex> ok_then({:ok, 5}, fn x -> {:ok, x * 2} end)
{:ok, 10}

iex> ok_then({:ok, 5}, fn _x -> raise "boom" end)
{:error, %Triage.WrappedError{}}

ok_then!(result, func, opts \\ [])

@spec ok_then!(result(), (any() -> any()), [{:retries, non_neg_integer()}]) ::
  result()

Executes a function with a previous result value, without exception handling.

Takes a result and, if it's an :ok, passes the value from the tuple to the provided function. If the result was an error, short-circuits and returns the error without calling the function.

This is the "unsafe" version that doesn't catch exceptions. Use ok_then/2 for exception handling.

Parameters

  • result - The previous result (:ok, {:ok, value}, :error, or {:error, reason})
  • func - A function that takes the unwrapped value and returns a result

Examples

iex> ok_then!({:ok, 5}, fn x -> {:ok, x * 2} end)
{:ok, 10}

iex> ok_then!({:error, :not_found}, fn x -> {:ok, x * 2} end)
{:error, :not_found}

run(func, opts \\ [])

@spec run((-> any()), [{:retries, non_neg_integer()}]) :: result()

Executes a function that returns a result tuple, with exception handling.

Calls the provided zero-arity function and ensures it returns a valid result. If the function raises an exception, it catches it and returns {:error, %Triage.WrappedError{}} with details about the exception.

Parameters

  • func - A zero-arity function that returns a result

Examples

iex> run(fn -> {:ok, 42} end)
{:ok, 42}

iex> run(fn -> raise "boom" end)
{:error, %Triage.WrappedError{}}

run!(func, opts \\ [])

@spec run!((-> any()), [{:retries, non_neg_integer()}]) :: result()

Executes a function that returns a result tuple, without exception handling.

Calls the provided zero-arity function and checks that it returns a result (:ok, {:ok, value}, :error, or {:error, reason}). If the function returns any other value, it wraps it in {:ok, value}.

This is the "unsafe" version that doesn't catch exceptions. Use run/1 for exception handling.

Parameters

  • func - A zero-arity function that returns a result

Examples

iex> run!(fn -> 42 end)
{:ok, 42}

iex> run!(fn -> {:ok, 42} end)
{:ok, 42}

iex> run!(fn -> {:error, :not_found} end)
{:error, :not_found}

iex> run!(fn -> :error end)
:error

iex> run!(&function_to_try, retries: 2)
:error

tap_error(result, func)

@spec tap_error(result(), (any() -> any())) :: result()

Executes a side-effect function when the result is an error, passing the result through unchanged.

Takes a result and, if it's :error or {:error, reason}, calls the provided function with the unwrapped reason (or nil for :error). The result is always returned unchanged, making this useful for side effects like logging or debugging errors.

If the result is :ok, the function is not called and the :ok result is returned unchanged.

Examples

# Monitor payment failures and send alerts
iex> process_payment(order)
...> |> tap_error(fn reason ->
...>   ErrorTracker.report(reason, context: %{order_id: order.id})
...>   if reason == :payment_gateway_down, do: PagerDuty.alert("Gateway down")
...> end)
{:error, :payment_gateway_down}

# Track metrics for failed operations
iex> update_inventory(product_id, quantity)
...> |> tap_error(fn reason ->
...>   ErrorTracker.report(reason, context: %{product_id: product_id})
...>   Metrics.increment("inventory.update.failed", tags: %{reason: reason})
...> end)
{:error, :insufficient_stock}

# Success results pass through without calling the function
iex> tap_error({:ok, user}, fn _ -> ErrorTracker.report("Won't be called") end)
{:ok, %User{}}

tap_ok(result, func)

@spec tap_ok(result(), (any() -> any())) :: result()

Executes a side-effect function when the result is :ok, passing the result through unchanged.

Takes a result and, if it's :ok or {:ok, value}, calls the provided function with the unwrapped value (or nil for :ok). The result is always returned unchanged, making this useful for side effects like logging or debugging.

If the result is an error, the function is not called and the error is returned unchanged.

Examples

# Track analytics for successful user registrations
iex> register_user(params)
...> |> tap_ok(fn user -> Analytics.track("user_registered", %{user_id: user.id}) end)
{:ok, %User{email: "user@example.com"}}

# Cache API responses while continuing the pipeline
iex> fetch_product_from_api(product_id)
...> |> tap_ok(fn product -> Cache.put("product:#{product_id}", product) end)
{:ok, %Product{id: 123, name: "Widget"}}

# Errors pass through without calling the function
iex> tap_ok({:error, :not_found}, fn user -> send_notification(user) end)
{:error, :not_found}

Functions > Enumeration

all(error, func)

@spec all(value_result() | Enumerable.t(), (any() -> result())) ::
  :ok | :error | {:error, Triage.WrappedError.t()}

Validates that a callback function gives an :ok / {:ok, _} result for all elements in the given enumerable.

Takes an enumerable or {:ok, enumerable} (giving :ok will return an error) and applies a callback function to each element. If all calls to the callback return :ok or {:ok, any()} then all returns :ok.

If any callback returns an error, immediately stops processing and returns that error.

If :error or {:error, reason} are given as the first argument, they are returned unchanged. Note that even if callbacks return {:ok, value}, the values are discarded and only :ok is returned — this function is for validation, not transformation. See map_if/2 if you need transformation which short-circuits.

This is useful when you need to validate that all items in a collection meet certain criteria before proceeding with subsequent operations.

Examples

iex> Triage.all(emails, &check_valid_email)
:ok

iex> Triage.all({:ok, emails}, &check_valid_email)
{:error, :invalid_hostname}

iex> Triage.all(:error, fn _ -> <not called> end)
:error

iex> Triage.all({:error, :not_found}, fn _ -> <not called> end)
{:error, :not_found}

find_value(error, func)

@spec find_value(value_result() | Enumerable.t(), (any() -> result())) ::
  :ok | {:ok, any()} | {:error, [any()]}

Finds the first successful result from applying a function to enumerable elements.

Takes an enumerable or {:ok, enumerable} and applies a function to each element The first successful result (:ok or {:ok, value}) from the callback is returned from find_value and no further iteration is done.

If all callbacks return errors, then {:error, [list of error reasons]} is returned. For :error atoms in the error list, they are represented as nil.

If :error or {:error, reason} is given as the first argument to find_value, it is passed through unchanged.

This can be useful when you're trying multiple strategies or checking multiple values to find the first one that works.

Examples

iex> Triage.find_value(domains, &ping_domain)
{:ok, "www.mydomain.com"}

iex> Triage.find_value({:ok, domains}, &ping_domain)
{:error, [:nxdomain, :timeout, :nxdomain]}

iex> Triage.find_value(:error, fn _ -> <not called> end)
:error

iex> Triage.find_value({:error, :not_found}, fn _ -> <not called> end)
{:error, :not_found}

map_if(error, func)

@spec map_if(result(), (any() -> any())) :: result()

Maps a function over an enumerable, collecting successful values and short-circuiting on the first error.

Takes an enumerable or {:ok, enumerable} and applies a function to each element. If all callbacks return success with {:ok, value}), map_if returns {:ok, [transformed_values]}. If any call to the callback returns an error, map_if immediately stops processing and returns that error.

If map_if is given an :error result for it's first argument that argument is returned unchanged and the callback is never called.

Since map_if requires an enumerable value to work with, it will fail if given :ok as a result argument.

This is useful when you need all transformations to succeed—if any fail, you don't want the partial results.

Examples

iex> Triage.map_if(xml_docs, & xml_to_json(&1, opts))
{:ok, [...]}

iex> Triage.map_if(xml_docs, & xml_to_json(&1, opts))
{:error, ...}

iex> Triage.map_if(:error, fn _ -> <not called> end)
:error

iex> Triage.map_if({:error, :not_found}, fn _ -> <not called end)
{:error, :not_found}

Functions > Helpers

error?(result)

Checks if a result is an error (:error or {:error, any()}).

Returns true if the result is :error or {:error, any()}, false if it's :ok or {:ok, any()}. Raises ArgumentError for any other value.

Examples

iex> Triage.error?(:error)
true

iex> Triage.error?({:error, :not_found})
true

iex> Triage.error?({:error, :not_found, 123})
true

iex> Triage.error?(:ok)
false

iex> Triage.error?({:ok, 42})
false

iex> Triage.error?({:ok, 42, :ignored})
false

log(result, mode \\ :errors)

Logs a result tuple and returns it unchanged.

Takes a result and logs it. By default, only errors are logged.

The mode argument can be either :errors (the default) or :all (logs all results)

See this guide for information about logging with JSON

The result passed in can be:

  • :ok / :error
  • {:ok, any()} / {:error, any()}
  • {:ok, ...} / {:error, ...} (any sized tuple starting with :ok or :error)

ok?(result)

Checks if a result is a success (:ok or {:ok, any()}).

Returns true if the result is :ok or {:ok, any()}, false if it's :error or {:error, any()}. Raises ArgumentError for any other value.

Examples

iex> Triage.ok?(:ok)
true

iex> Triage.ok?({:ok, 42})
true

iex> Triage.ok?({:ok, 42, :ignore})
true

iex> Triage.ok?(:error)
false

iex> Triage.ok?({:error, :not_found})
false

iex> Triage.ok?({:error, :not_found, 123})
false

user_message(arg)

@spec user_message({:error, any()}) :: String.t()

Generates a user-friendly error message from various error types.

Converts {:error, reason} tuples into human-readable messages suitable for displaying to end users.

When the reason is a string, the string error message is returned.

When the reason is Triage.WrappedError.t/0 it unwraps the error chain and includes context information in the message.

For exceptions and unknown error types, it

  • generates a unique error code
  • logs the error code with full error details
  • returns a generic error to the user with the error code that the user can report

There is also logic to specifically deal with Ecto.Changeset validation errors so that you don't need to implement your own (at least as long as you're happy with the default behavior)

Parameters

  • reason - The error to convert (string, exception, %Triage.WrappedError{}, or any other value)

Examples

iex> user_message({:error, "Invalid email"})
"Invalid email"

# WrappedError which was returned as a result of `wrap_context` being called in two places
# and where the original error was `{:error, "not found"}`
iex> user_message({:error, %Triage.WrappedError{}})
"not found (happened while: fetching user => validating email)"

# Ecto.Changeset with validation errors on multiple fields
iex> user_message({:error, %Ecto.Changeset{errors: [...]}})
"email: has invalid format, should be at least 10 character(s); name: can't be blank"

iex> user_message({:error, %RuntimeError{message: "boom"}})
"There was an error. Refer to code: ABC12345"

Log generated:
ABC12345: Could not generate user error message. Error was: #RuntimeError<...> (message: boom)