Elixir Cheatsheet

Error Handling

Use this Elixir reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

The {:ok, _} / {:error, _} Convention

case File.read("config.json") do
  {:ok, contents} -> parse(contents)
  {:error, :enoent} -> {:error, :missing_config}
  {:error, reason} -> {:error, reason}
end

Expected failures return tagged tuples; exceptions are for bugs and truly unexpected states. Library pairs follow a naming rule: File.read/1 returns tuples, File.read!/1 (bang) returns the value or raises.

Chaining with with

with {:ok, body} <- File.read(path),
     {:ok, json} <- Jason.decode(body),
     %{"id" => id} <- json do
  {:ok, id}
else
  {:error, %Jason.DecodeError{}} -> {:error, :bad_json}
  {:error, reason} -> {:error, reason}
  _ -> {:error, :invalid_shape}
end

with is the idiomatic happy-path pipeline for functions that return tagged tuples; the first non-matching step falls through to else (or is returned as-is when there is no else).

raise and rescue

raise "boom"                                  # RuntimeError
raise ArgumentError, "expected a positive integer"

try do
  String.to_integer(input)
rescue
  e in ArgumentError -> {:error, Exception.message(e)}
  _e in [KeyError, MatchError] -> {:error, :bad_data}
end

rescue matches exception structs by module. In most Elixir code try/rescue is rare — let it crash and supervise, or return tagged tuples.

try / after / else

{:ok, file} = File.open(path)

try do
  IO.read(file, :eof)
after
  File.close(file)              # always runs (cleanup)
end

try do
  compute(x)
rescue
  ArithmeticError -> :error
else
  result when result > 0 -> {:ok, result}   # runs when no exception was raised
  _ -> {:ok, 0}
end

Custom Exceptions

defmodule PaymentError do
  defexception [:code, message: "payment failed"]
end

raise PaymentError, code: :card_declined

# custom message from fields
defmodule NotFoundError do
  defexception [:id]

  @impl true
  def message(%{id: id}), do: "record #{id} not found"
end

defexception defines an exception struct (it is a struct with a :message contract). Convert any exception to text with Exception.message/1.

throw / catch and exit

try do
  Enum.each(1..100, fn x ->
    if found?(x), do: throw({:found, x})
  end)
  :not_found
catch
  {:found, x} -> {:ok, x}
end

exit(:normal)                 # terminate the current process
exit({:shutdown, :maintenance})

try do
  GenServer.call(pid, :req)
catch
  :exit, {:timeout, _} -> {:error, :timeout}   # catch kind, reason
end

throw is non-local return (rare; prefer Enum.reduce_while/3). exit signals process termination and is normally handled by supervisors, not caught.

Errors Across Processes

{:ok, pid} = Task.start_link(fn -> raise "boom" end)   # crash propagates to linked parent
ref = Process.monitor(pid)                             # monitor: get a message instead
receive do
  {:DOWN, ^ref, :process, _pid, reason} -> {:crashed, reason}
end

The OTP philosophy: don't defensively rescue everything — isolate risky work in a process, link or monitor it, and let supervisors restart clean state. See Processes and OTP.