README.md

# Triage

A lightweight Elixir library for enhanced handling of **results** (`{:ok, _}` / `:ok` / `{:error, _}` / `:error`) with context wrapping, logging, and user message generation.

## Features

This package provides three levels of working with errors which are all **usable independently**, but which all complement each other.

- **Context Wrapping**: Add meaningful context to errors as they bubble up through your application
- **Result Logging**: Log errors (and optionally successes) with file/line information
- **User-friendly errors**: Be able to collapse errors into a single user error message
- **Error control flow**: `then` and `handle` functions help control and transform results
- **Error enumeration**: functions like `map_unless`, `find_value`, and `all` help deal with enumerations over data where each iteration may succeed or fail.

Design goals:

- Standard results (`:ok`, `:error`, `{:ok, term()}`, `{:error, term()}` with only one value in tuples)
- Avoid macros for easy pick-up-and-use throughout a codebase (i.e. no need for `require`)
- Variety of small tools which work well together (like UNIX commands)

See the [Philosophy](https://hexdocs.pm/triage/philosophy.html) section of the docs for more details.

## Examples

### Contexts

When an error is returned (e.g. in a tuple, as opposed to being raised), often that error can be passed up a stack and soon the context of where it came from can be lost. Triage offers a `wrap_context` function to attach a context string and/or metadata to errors via a `WrappedError` exception struct, as well as `log` and `user_message` functions which can take advantage of this extra information to assist debugging.

Note that while the `user_message` function supports turning `WrappedError` structs into useful human-readable strings, it supports giving user error messages for any error tuple.

```elixir
defmodule MyApp.OrderProcessor do
  def process_payment(order) do
    with {:ok, payment_method} <- fetch_payment_method(order),
         {:ok, charge} <- charge_payment(payment_method, order.amount) do
      {:ok, charge}
    end
    |> Triage.wrap_context("process payment", %{order_id: order.id, order_amount: order.amount})
  end
  # ...
end

defmodule MyApp.OrderService do
  def complete_order(order_id) do
    fetch_order(order_id)
    |> MyApp.OrderProcessor.process_payment()
    |> Triage.wrap_context("complete order")
  end
  # ...
end
```

But an error wrapped with a context isn't so useful by itself.  Your code can look at the `WrappedError` if you'd like, but it can be most useful with the output tools below.

(Also, make sure to see the [Contexts section of the docs](https://hexdocs.pm/triage/contexts.html) for more information)

### Output

Error results that you get back can be a mess. Often when you get an error tuple, it comes back as the result of a tree of nested calls and the reason value could be of many types.  So it's useful to have tools which let you not worry about it so much. Below is an example of using `Triage.log` and `Triage.user_message` in a Phoenix controller:

```elixir
def show(conn, %{"order_id" => order_id}) do
  order_id = String.to_integer(order_id)

  MyApp.complete_order(order_id)
  |> Triage.log()
  # ...
```

By default `Triage.log` will only output error cases, so if this case is important we can have a log of how it went wrong. Also note that any metadata given to `log` is also assigned to the [Logger metadata](https://hexdocs.pm/logger/Logger.html#module-metadata) in addition to being outputted (helpful for filtering logs).

The output can be as simple as this in the case of an atom given as the error reason:

```
[RESULT] lib/my_app/order_controller.ex:41: {:error, :order_was_invalid}
```

But if `Triage.wrap_context` is used, we can get even more details out:

```
[error] [RESULT] lib/my_app/order_service.ex:15: {:error, :payment_declined}
  [CONTEXT] lib/my_app/order_service.ex:15: complete order
  [CONTEXT] lib/my_app/order_processor.ex:8: process payment | %{order_id: 12345, amount: 99.99}
```

Note that if you'd prefer to output JSON logs, there is some [information in the docs](https://hexdocs.pm/triage/logging-json.html)

Additionally, the `Triage.user_message` function will extract a message from the error if possible.  If not possible, the user will be given a generic error with a randomly generated short code which can be matched to a log entry with details about the error.

```elixir
def show(conn, %{"order_id" => order_id}) do
  order_id = String.to_integer(order_id)

  MyApp.complete_order(order_id)
  |> case do
    {:ok, value} ->
      # ...

    {:error, reason} ->
      conn
      |> put_status(400)
      |> json(%{error: Triage.user_message(reason)})
  end
  # ...
```

See the [Outputs section of the docs](https://hexdocs.pm/triage/outputs.html) for more information.

### Enumeration

`triage` has a set of functions to help when you have a series of step which might succeed or fail.  As an example, you may want to build up a list, but return an error if anything fails.

```elixir
  defp validate_each_metric(metrics, query) do
    Enum.reduce_while(metrics, {:ok, []}, fn metric, {:ok, acc} ->
      case validate_metric(metric, query) do
        {:ok, metric} -> {:cont, {:ok, acc ++ [metric]}}
        {:error, reason} -> {:halt, {:error, reason}}
      end
    end)
  end
```

The `Triage.map_unless` function is one tool available:

```elixir
  defp validate_each_metric(metrics, query) do
    # Returns {:ok, [...]} where the original returned just [...]
    Triage.map_unless(metrics, & validate_metric(&1, query))
  end
```

For more functions and examples, see the [Enumerating Errors section of the docs](https://hexdocs.pm/triage/enumerating-errors.html).

### Control Flow

`triage`'s two control flow tools (`then` and `handle`) can both be shown via an HTTP request example:

```elixir
HTTPoison.get(url)
|> Triage.then(fn
  %HTTPoison.Response{status_code: 200, body: body} ->
    body

  %HTTPoison.Response{status_code: 404, body: body} ->
    {:error, "Server result not found"}
end)
|> Triage.handle(fn
    %HTTPoison.Error{reason: :nxdomain} ->
      "Server domain not found"

    %HTTPoison.Error{reason: :econnrefused} ->
      "Server connection refused"

    %HTTPoison.Error{reason: reason} ->
      "Unexpected error connecting to server: #{inspect(reason)}"
end)
```

The `Triage.then` function works on `:ok` results, ignoring errors.  Values that are returned from the callback are automatically wrapped in an `{:ok, _}` tuple, though any `:error` or `{:error, term()}` returned will be returned as an error.

The `Triage.handle` function is the opposite: working on `:error` reasons and returning new reasons to be wrapped in an `{:error, _}` tuple.  If an `:ok` or `{:ok, _}` result is returned, then the error is ignored and `Triage.handle` will return that success.

Make sure to see the [Control Flow section of the docs](https://hexdocs.pm/triage/control-flow.html) for more information.

Also, many people wonder why they shouldn't just use `with` instead of `then` / `handle`.  There is a [section in the docs](https://hexdocs.pm/triage/comparison-to-with.html) for that too!

## Installation

Add `triage` to your list of dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:triage, "~> 0.2.0"}
  ]
end
```

For various reasons, `triage` requires at least version `1.15` of Elixir.

## Usage

See [the docs](https://hexdocs.pm/triage) for detailed information about the different tools available.

## Development

Run tests:

Run tests in watch mode (uses [`mix_test_interactive`](https://hex.pm/packages/mix_test_interactive):

```bash
mix test.interactive
```

Or just:

```bash
mix test
```

## License

Copyright (c) 2025

This work is free. You can redistribute it and/or modify it under the
terms of the MIT License. See the LICENSE file for more details.