पाठ 9 / 25

Errors: Tagged Tuples, Exceptions and Bang Functions

Handle expected and unexpected failures idiomatically.

Expected errors versus exceptional ones

Elixir distinguishes expected failures, which are part of normal flow, from unexpected ones. Expected failures are returned as tagged tuples, {:ok, value} or {:error, reason}, and handled with case or with. Many library functions come in two forms: File.read/1 returns a tuple, while File.read!/1 (a bang function) returns the value directly or raises an exception. Use the bang version when failure means a bug or a broken environment and you have no useful way to recover. Exceptions are raised with raise and handled with try/rescue, and custom exceptions are defined with defexception. However, in Elixir try/rescue is used sparingly: the OTP philosophy is "let it crash". Instead of defensive code for every possible error, write the happy path, let a process crash on unexpected failures and rely on a supervisor to restart it in a known good state. Also available are throw/catch (rare, for non-local returns) and exits, which are signals between processes.

Tuples, bang functions and custom exceptions

Return tuples for expected errors, raise for bugs.

defmodule Shop.InvalidCoupon do
  defexception [:code, message: "invalid coupon"]

  @impl true
  def message(%{code: code}), do: "invalid coupon: #{code}"
end

defmodule Shop.Coupons do
  @coupons %{"FESTIVE10" => 10, "WELCOME5" => 5}

  def fetch(code) do
    case Map.fetch(@coupons, String.upcase(code)) do
      {:ok, percent} -> {:ok, percent}
      :error -> {:error, :unknown_coupon}
    end
  end

  def fetch!(code) do
    case fetch(code) do
      {:ok, percent} -> percent
      {:error, _} -> raise Shop.InvalidCoupon, code: code
    end
  end
end

case Shop.Coupons.fetch("festive10") do
  {:ok, percent} -> IO.puts("#{percent}% off")
  {:error, :unknown_coupon} -> IO.puts("Coupon not recognised")
end

case File.read("config/prices.json") do
  {:ok, body} -> IO.puts("read #{byte_size(body)} bytes")
  {:error, reason} -> IO.puts("cannot read prices: #{:file.format_error(reason)}")
end

try do
  Shop.Coupons.fetch!("BOGUS")
rescue
  e in Shop.InvalidCoupon -> IO.puts(Exception.message(e))
end

Let it crash, but not everywhere

Let it crash applies to processes under supervision handling unexpected states. User input errors, such as an invalid coupon, are expected and should return {:error, reason} with a helpful message.

त्वरित जाँच: What does a bang function such as File.read!/1 do on failure?

  • Returns {:error, reason}
  • Returns nil
  • Raises an exception
  • Retries forever
Answer

Raises an exception — Bang functions return the plain value or raise.