100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Fault Tolerance in Elixir

Explore how links, monitors, and the let-it-crash philosophy combine to make Elixir systems resilient by design.

Concurrency & OTPAdvanced11 min readJul 10, 2026
Analogies

The Let It Crash Philosophy

Elixir's fault-tolerance philosophy, inherited from Erlang, deliberately encourages writing 'happy path' code and letting unexpected errors crash the process rather than defensively coding around every conceivable edge case, because a crashed process that gets cleanly restarted by a supervisor is often more reliable than a process that limped on in a corrupted, half-handled state. This doesn't mean ignoring errors you can meaningfully recover from locally -- it means reserving try/rescue for truly expected, recoverable conditions and trusting supervision for everything else.

🏏

Cricket analogy: A bowler who oversteps and gets a no-ball called doesn't try to secretly finish the delivery as if it were legal -- the umpire stops it cleanly and the bowler restarts the run-up fresh, which is far safer than play continuing on a delivery everyone knows was compromised, like a phantom Mitchell Starc no-ball wicket being overturned rather than left standing.

A link, created implicitly by spawn_link/1,3 or explicitly by Process.link/1, creates a bidirectional relationship where if either linked process crashes, the other receives an exit signal too -- by default that exit signal kills the linked process unless it's trapping exits, which is exactly the mechanism supervisors rely on to learn when a child has died. A monitor, created with Process.monitor/1, is one-directional and non-lethal: the monitoring process receives a {:DOWN, ref, :process, pid, reason} message when the monitored process terminates, but its own execution is never interrupted, making monitors the right tool when you need to observe a process without being tied to its fate.

🏏

Cricket analogy: Two batsmen at the crease are linked -- if one gets run out attempting a risky second run, both their partnership and momentum are affected together, whereas a commentator watching the match from the box is like a monitor, getting notified the instant a wicket falls without their own broadcast being interrupted by it.

elixir
# Linked: if worker crashes, this process is also terminated (unless trapping exits)
worker_pid = spawn_link(fn -> risky_work() end)

# Monitored: this process just gets a message, it is not terminated
{monitor_pid, ref} = spawn_monitor(fn -> risky_work() end)

receive do
  {:DOWN, ^ref, :process, ^monitor_pid, reason} ->
    IO.puts("Worker went down: #{inspect(reason)}")
end

Trapping Exits and Custom Recovery

A process can call Process.flag(:trap_exit, true) to convert incoming exit signals from links into ordinary {:EXIT, pid, reason} messages delivered to its mailbox instead of being killed outright, which is precisely how Supervisor itself stays alive when a child dies -- it traps exits, receives the {:EXIT, ...} message, and decides how to react according to its restart strategy rather than crashing alongside its child. Outside of writing supervisor-like infrastructure yourself, trapping exits is relatively rare in application code, since OTP's built-in Supervisor already handles this pattern correctly and battle-tested.

🏏

Cricket analogy: A team manager who chooses to personally review every player injury report instead of automatically substituting is like trap_exit -- rather than the team being pulled apart the moment a player goes down, the manager gets a full report and decides the right response, exactly how a captain like Eoin Morgan might manage an injury with more nuance than an automatic withdrawal.

elixir
defmodule SimpleWatcher do
  def start do
    Process.flag(:trap_exit, true)
    child = spawn_link(fn -> :timer.sleep(1_000); exit(:boom) end)
    loop(child)
  end

  defp loop(child) do
    receive do
      {:EXIT, ^child, reason} ->
        IO.puts("Child exited with reason: #{inspect(reason)}, restarting...")
        new_child = spawn_link(fn -> :timer.sleep(1_000); exit(:boom) end)
        loop(new_child)
    end
  end
end

Defensive Code vs Supervised Recovery

The discipline of 'let it crash' doesn't mean skipping input validation or reasonable error handling -- a function that receives obviously bad user input should still return a clear {:error, reason} tuple that its caller can pattern-match on, because that's an expected, everyday outcome, not an exceptional crash-worthy condition. What you skip is the defensive sprawl of checking for every conceivable database-connection failure, network timeout, or unexpected nil deep inside business logic; instead you let those genuinely exceptional cases raise, trust the supervisor to notice and restart, and keep your core logic focused on the actual happy-path problem you're solving.

🏏

Cricket analogy: A batsman leaving a ball outside off stump is a normal, expected outcome handled routinely, not a crisis -- but a freak equipment failure like a bat handle snapping mid-shot is a genuine exceptional event that stops play entirely rather than being quietly worked around.

Never blanket-wrap large chunks of business logic in try/rescue just to 'be safe' -- swallowing exceptions defeats the supervisor's ability to detect real failures and often leaves a process limping along in a state its own code never anticipated, which is usually worse than the crash you were trying to avoid.

  • Let it crash means writing happy-path code for expected logic and trusting supervision for truly exceptional failures.
  • Links are bidirectional and lethal by default: a crash in either linked process propagates to the other.
  • Monitors are one-directional and non-lethal: the monitor receives a {:DOWN, ...} message without being terminated.
  • Process.flag(:trap_exit, true) converts incoming exit signals into ordinary {:EXIT, ...} messages instead of killing the process.
  • Supervisor itself relies on trapping exits to detect child crashes and apply its restart strategy.
  • Expected, recoverable conditions should return explicit {:error, reason} tuples rather than crashing.
  • Genuinely exceptional failures (bad connections, unexpected nils deep in logic) should be allowed to raise and be handled by supervision.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#ElixirStudyNotes#FaultToleranceInElixir#Fault#Tolerance#Elixir#Let#StudyNotes#SkillVeris#ExamPrep