Elixir Fundamentals: Functional Programming and OTP · Processes, OTP and Fault Tolerance
Errors, Tagged Tuples and 'Let It Crash'
Use {:ok, value} / {:error, reason} for expected failures, raise and rescue for exceptional ones, define custom exceptions, and understand why Elixir code often lets processes crash and restarts them instead of defending against every error.
What you'll build
A file-importer core that returns tagged tuples for expected problems, raises a custom exception for a broken invariant, and shows the convention of function vs function! APIs.
The idea
Elixir has two error styles:
- Expected failures (bad input, missing records, network errors) return
{:ok, value}or{:error, reason}. Callers pattern-match. - Exceptional situations raise exceptions (
raise ArgumentError, "...", or your owndefexception). Rescue them withtry/rescueonly at boundaries.
Many libraries offer pairs: File.read/1 returns tuples; File.read!/1 returns the value or raises. Use the bang version when failure means the program can't sensibly continue.
"Let it crash": instead of wrapping every operation in defensive code, Elixir apps run work in isolated processes watched by supervisors. If a process hits an unexpected state, it crashes and the supervisor restarts it in a known-good state. Defensive code goes where you can actually do something useful (show the user an error, retry); everything else is allowed to fail fast.
The code
defmodule ImportError do
defexception [:line, :reason]
@impl true
def message(%{line: line, reason: reason}), do: "line #{line}: #{reason}"
end
defmodule Importer do
def parse_line(line) do
case String.split(line, ",") do
[name, qty] ->
case Integer.parse(String.trim(qty)) do
{n, ""} when n >= 0 -> {:ok, {String.trim(name), n}}
_ -> {:error, :bad_quantity}
end
_ ->
{:error, :wrong_column_count}
end
end
def parse_line!(line, number) do
case parse_line(line) do
{:ok, row} -> row
{:error, reason} -> raise ImportError, line: number, reason: reason
end
end
def import(text) do
text
|> String.split("\n", trim: true)
|> Enum.with_index(1)
|> Enum.reduce(%{rows: [], errors: []}, fn {line, n}, acc ->
case parse_line(line) do
{:ok, row} -> %{acc | rows: [row | acc.rows]}
{:error, reason} -> %{acc | errors: [{n, reason} | acc.errors]}
end
end)
|> then(fn acc -> %{rows: Enum.reverse(acc.rows), errors: Enum.reverse(acc.errors)} end)
end
end
csv = "apples, 12\npears, ten\nplums\ncherries, 40"
IO.inspect(Importer.import(csv))
IO.inspect(Importer.parse_line!("figs, 3", 1))
try do
Importer.parse_line!("dates, -4", 7)
rescue
e in ImportError -> IO.puts("rescued: " <> Exception.message(e))
end
{:error, reason} = File.read("/definitely/missing.txt")
IO.inspect(reason, label: "File.read")
Output
%{
rows: [{"apples", 12}, {"cherries", 40}],
errors: [{2, :bad_quantity}, {3, :wrong_column_count}]
}
{"figs", 3}
rescued: line 7: bad_quantity
File.read: :enoentRun this on your computer — see “Get Started” for setup.
Common mistakes
try/rescueeverywhere, like exceptions in other languages; prefer tagged tuples and let supervisors handle the unexpected.- Raising for expected cases (a user typed a bad value); return
{:error, reason}. - Swallowing errors with catch-all clauses that hide bugs.
- Mixing styles within one API; offer
fun/1(tuples) andfun!/1(raises) consistently.
Key points
- Expected failures →
{:ok, v}/{:error, r}; exceptional ones →raise. fun!/1variants raise instead of returning error tuples.defexceptionfor custom exceptions; rescue only at boundaries.- "Let it crash": isolate work in supervised processes and restart cleanly.
Further reading: try, catch, and rescue.
Practice questions
What does 'let it crash' mean in Elixir?
Answer
Rather than defending against every possible error, code assumes the happy path and lets a process crash on unexpected states. A supervisor restarts it in a known-good state, which keeps the system running and the code simpler. Expected errors are still handled with tagged tuples.
What's the convention behind File.read/1 and File.read!/1?
Answer
The plain version returns {:ok, value} or {:error, reason}; the bang version returns the value directly or raises an exception on failure.
Open in the app to mark lessons complete, take notes and track your progress.