Lesson 7 / 25

case, cond, if and with

Choose the right control-flow construct and chain fallible steps with with.

Branching in a functional language

case matches a value against patterns (with optional guards) and is the workhorse for handling results: case fetch(id) do {:ok, user} -> ...; {:error, :not_found} -> ... end. cond checks a series of boolean conditions and runs the first true one, useful when you have ranges or unrelated tests; end with true -> as a catch-all. if and unless handle a single condition, and only false and nil are falsy. All of these are expressions that return values. with chains several steps that each must match a pattern: if every clause matches, the do block runs; if one fails, the non-matching value is returned (or handled in an else block). It is the idiomatic way to write a happy path through several {:ok, value} steps without nested case statements. Prefer multi-clause functions when branching on the shape of an argument, case for results of a call, cond for multiple conditions and with for sequences of fallible steps.

The with happy path

Each step must match {:ok, value}; the first failure exits to else.

A straight horizontal path of four checkpoints with a branching exit arrow from each checkpoint down to a shared error box.
Figure 3.1 — with short-circuits on the first non-matching step.

Placing an order with with

Each step returns {:ok, value} or {:error, reason}.

defmodule Shop.Checkout do
  def place_order(params) do
    with {:ok, customer} <- find_customer(params["customer_id"]),
         {:ok, items} <- validate_items(params["items"]),
         total = Enum.sum(Enum.map(items, & &1.price * &1.qty)),
         :ok <- check_credit(customer, total) do
      {:ok, %{customer: customer.name, total: total}}
    else
      {:error, :not_found} -> {:error, "customer not found"}
      {:error, :empty} -> {:error, "cart is empty"}
      {:error, :credit_limit} -> {:error, "credit limit exceeded"}
    end
  end

  defp find_customer("c-1"), do: {:ok, %{id: "c-1", name: "Asha", limit: 500_000}}
  defp find_customer(_), do: {:error, :not_found}

  defp validate_items([]), do: {:error, :empty}
  defp validate_items(nil), do: {:error, :empty}
  defp validate_items(items), do: {:ok, items}

  defp check_credit(%{limit: limit}, total) when total <= limit, do: :ok
  defp check_credit(_customer, _total), do: {:error, :credit_limit}

  def shipping_label(total) do
    cond do
      total >= 100_000 -> "free"
      total >= 50_000 -> "discounted"
      true -> "standard"
    end
  end
end

Shop.Checkout.place_order(%{"customer_id" => "c-1", "items" => [%{price: 4_950, qty: 2}]})
|> IO.inspect()                      # {:ok, %{customer: "Asha", total: 9900}}

Keep with else clauses exhaustive

If a with step returns something that no else clause matches, Elixir raises WithClauseError. Make step functions return a small, known set of error tuples, or add a final catch-all clause.

Quick check: What happens when a clause inside with does not match?

  • with stops and returns the non-matching value, or passes it to the else block
  • The program crashes always
  • The clause is skipped and the next one runs
  • It retries the clause
Answer

with stops and returns the non-matching value, or passes it to the else block — with short-circuits on the first non-matching value.