TaggedWith

A with that tags each clause's failure by name, so else can tell which step failed even when two clauses can fail with the same-looking value.

Plain with matches else on whatever shape a clause's expression happens to return. That breaks down when two clauses can produce the same-looking value — say, validate_email/1 and validate_age/1 both returning {:error, :invalid}. A pattern like {:error, :invalid} in else can't tell you which one failed.

Tagging each clause fixes that — just {name, pattern} <- {name, expr} — but it means writing the name twice per clause, by hand, every time:

# what you'd otherwise write by hand
with {:email, :ok} <- {:email, validate_email(params)},
{:age, :ok} <- {:age, validate_age(params)} do
:ok
else
{:email, {:error, reason}} -> {:error, {:invalid_email, reason}}
{:age, {:error, reason}} -> {:error, {:invalid_age, reason}}
end
# what you actually write
import TaggedWith
tagged_with email: :ok <- validate_email(params),
age: :ok <- validate_age(params) do
:ok
else
{:email, {:error, reason}} -> {:error, {:invalid_email, reason}}
{:age, {:error, reason}} -> {:error, {:invalid_age, reason}}
end

Each clause is name: pattern <- expr — no parens needed around pattern <- expr, it parses fine as a keyword value. Write the bare pattern you'd use in a plain with (:ok, {:ok, value}, ...) — tagged_with adds the name to both sides for you, expanding into exactly the hand-written form above, so else can still catch and tell apart the failure from each line.

else is optional, exactly like in plain with. Without it, a failed clause's tagged value (e.g. {:email, {:error, reason}}) comes back directly — no exception, no extra behavior, since that's already how with behaves without else.

Installation

Add tagged_with to your list of dependencies in mix.exs:

def deps do
[
{:tagged_with, "~> 0.1.0"}
]
end

Docs: https://hexdocs.pm/tagged_with