Stop Defensive Coding: Implementing Erlang Supervision Trees
Stop using try-catch for everything. Learn how Erlang's 'Let it Crash' philosophy and supervision trees create resilient systems by isolating failures and automating recovery.
23 Mar 2026, 19:13 UTC

The Cost of the 'Try-Catch' Obsession
In most languages, the instinct is to wrap every risky operation in a try-catch block. We spend hours anticipating every possible edge case to prevent a crash. However, this defensive approach often hides bugs and leaves the system in an inconsistent, "zombie" state where the process is alive but its internal data is corrupted.
Erlang solves this with a different thesis: Let it crash. Instead of trying to handle every possible error locally, you isolate the risk into a lightweight process and let a separate entity—a supervisor—handle the recovery. The goal isn't to prevent crashes, but to ensure the system returns to a known stable state immediately after one occurs.
Isolation via Shared-Nothing Architecture
The "Let it Crash" philosophy only works because Erlang uses a shared-nothing architecture. Each process has its own private heap and stack. When a process crashes due to a badmatch or a runtime error, it cannot corrupt the memory of another process. This isolation ensures that a failure in a single user session or a specific network socket doesn't bring down the entire virtual machine (BEAM).
Structuring Recovery with Supervision Trees
A supervision tree is a hierarchy where supervisor processes monitor worker processes. Supervisors do not perform business logic; their sole responsibility is to observe their children and restart them based on a specific strategy.
Choosing a Restart Strategy
The strategy you choose depends on how tightly coupled your worker processes are:
- one_for_one: If a worker crashes, only that worker is restarted. Use this for independent tasks, like handling individual HTTP requests.
- one_for_all: If one worker crashes, the supervisor kills and restarts all other children. Use this when workers are interdependent and cannot function if one of their peers is in an inconsistent state.
- rest_for_one: If a worker crashes, the supervisor restarts that worker and any workers started after it in the child specification.
Example: Building a Basic Supervisor
To implement this, you define a child specification that tells the supervisor how to start the worker and what to do when it fails. Run the following logic in the Erlang shell (erl) to see the mechanism in action.
% Define a simple worker that crashes when it receives 'kill'
-module(worker).
-export([start/0, loop/0]).
start() -> spawn(worker, loop, []).
loop() ->
receive
kill -> exit(crash_on_purpose);
Msg -> io:format("Received: ~p~n", [Msg]), loop()
end.
% Define a supervisor to manage the worker
-module(my_sup).
-export([start_link/0]).
start_link() ->
% Child spec: {ID, StartFunction, Shutdown, AuxiliaryArgs}
ChildSpec = #{id => worker_1,
start => {worker, start, []},
restart => permanent,
shutdown => 5000},
supervisor:start_link({local_name, my_supervisor}, [ChildSpec], {one_for_one, 1, 5}).
Verification:
- Run
my_sup:start_link().to start the supervisor and the worker. - Find the worker PID and send it the
killmessage. - Observe the supervisor logs or check the process list; the supervisor will immediately spawn a new
workerprocess to replace the crashed one.
The Critical Trade-offs
While powerful, this approach has specific limitations that can lead to system instability if ignored.
The Infinite Restart Loop
If a process crashes immediately upon startup (e.g., due to a missing configuration file), the supervisor will try to restart it indefinitely. To prevent this, supervisors use a max_restarts threshold. In the example above, {one_for_one, 1, 5} means if the process crashes more than 5 times within 1 second, the supervisor itself will give up and crash, escalating the failure up the tree to a higher-level supervisor.
State Loss
Because a crash wipes the process heap, any state held in the process memory is gone. If you need to persist data across crashes, you must use an external store like Mnesia (Erlang's distributed database) or a separate process designated as a state-holder that is not subject to the same crash triggers.
Actionable Summary
To move from defensive coding to a fault-tolerant architecture:
- Isolate: Move risky operations (parsing, network I/O) into their own processes.
- Categorize: Determine if your processes are independent (
one_for_one) or interdependent (one_for_all). - Limit: Set a conservative
max_restartsvalue to avoid CPU spikes during permanent failures. - Log: Since you are "letting it crash," ensure you have comprehensive logging of the exit reason to debug the root cause offline.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.