Lesson 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))
endLet 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.
Quick check: 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.