A language tour
Built for nine nines. Designed to crash gracefully and recover automatically. Telecom-grade fault tolerance in a language.
01 โ Let It Crash
Erlang's design philosophy inverts conventional wisdom. Don't write defensive code that tries to handle every failure. Instead, let processes crash when they encounter unexpected states, and rely on supervisors to restart them. The result is more reliable than defensive code โ simpler too.
"The problem with defensive programming is that you spend all your time handling errors that never happen, and your code gets complicated. In Erlang, you write for the happy path and let supervisors handle the rest."
โ Joe Armstrong, creator of Erlang% Processes communicate by message passing % If this crashes, the supervisor restarts it -module(worker). -export([start/0, loop/0]). start() -> spawn(fun loop/0). loop() -> receive {compute, Pid, X} -> % No nil check, no try/catch โ let it crash Result = X * X, Pid ! {result, Result}, loop(); stop -> ok end.
Erlang's message-passing model means processes share no memory. A crashing process cannot corrupt another process's state. Isolation is the foundation of fault tolerance.
02 โ Pattern Matching
Pattern matching in Erlang is pervasive โ function heads, receive clauses, case expressions, variable binding all use it. It's how Erlang describes what kind of message a process handles, and what to do with each shape of data.
% Function clauses match on argument shapes describe(0) -> "zero"; describe(N) when N > 0 -> "positive"; describe(_) -> "negative". % Destructure tuples and lists process({ok, Value}) -> {success, Value}; process({error, Reason}) -> {failure, Reason}. % In receive โ each message shape has its handler handle() -> receive {ping, From} -> From ! pong, handle(); {store, K, V} -> store(K, V), handle(); shutdown -> io:format("Shutting down~n") end.
Guards (when N > 0) extend pattern matching with arithmetic and type tests. Erlang function clauses are tried top-to-bottom โ the first matching clause executes.
03 โ Distributed Systems
Erlang was designed for distributed telephone switches. Sending a message to a process on another machine looks identical to sending it locally โ you use a Pid, and the runtime handles the network. Distributed Erlang is not an afterthought; it's a design goal.
% Start two Erlang nodes: % erl -sname node1@localhost % erl -sname node2@localhost % Connect nodes net_kernel:connect_node('node2@localhost'). % Spawn a process on a remote node RemotePid = spawn('node2@localhost', fun() -> receive {hello, From} -> From ! world end end). % Send a message โ works identically across the network RemotePid ! {hello, self()}. receive world -> io:format("Got world from the other node~n") end.
A process ID (Pid) in Erlang encodes which node it lives on. Pid ! Msg works whether the process is on the same node or on another machine halfway around the world.
04 โ Hot Code Reloading
Erlang can load new versions of modules into a running system while old processes continue using the old code. Telephone switches cannot be taken down for upgrades โ so Erlang made this a language feature. Systems can run for years without a restart.
% A server loop โ old version -module(server). -export([loop/1]). loop(State) -> receive {get, Pid} -> Pid ! State, loop(State); {set, Val} -> loop(Val); % Fully-qualified call triggers code upgrade % When a new module is loaded, this picks it up upgrade -> server:loop(State) end. % In the Erlang shell: % > l(server). % load new version % > Pid ! upgrade. % tell process to use it % The process continues โ no restart needed
The fully-qualified call server:loop(State) always calls the current version of the module. A plain loop(State) call stays on the old version. This is how Erlang managed rolling upgrades on telephone switches.
05 โ Supervisor Trees
OTP supervisors watch worker processes and restart them when they crash. Supervisors can watch other supervisors, forming trees. When a worker fails repeatedly, its supervisor fails too โ escalating the failure up the tree until something can actually handle it.
-module(my_sup). -behaviour(supervisor). -export([start_link/0, init/1]). start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> Children = [{ worker, % id {worker, start_link, []}, % {M, F, A} permanent, % always restart 5000, % shutdown timeout ms worker, % type [worker] % modules }], {ok, {{one_for_one, 5, 10}, Children}}.
one_for_one means: if one child crashes, restart only that child. {5, 10} means: if a child crashes 5 times in 10 seconds, the supervisor itself fails โ escalating to its own supervisor.
06 โ The Whole Picture
Built at Ericsson for telephone exchanges. The nine nines โ 99.9999999% uptime โ was a real requirement.
WhatsApp served 2 billion users with a tiny team. Discord handles millions of concurrent connections. Both on the BEAM.
Soft real-time scheduler, per-process GC, preemptive multitasking. The VM design is still state of the art.
The only mainstream language where zero-downtime code hot-swapping is a built-in, first-class feature.
Elixir runs on the BEAM and inherits all of OTP. Erlang's 35 years of battle-tested infrastructure, modern syntax.
A distributed database built into OTP. Real-time, transactional, replicated โ and it's part of the standard library.