programming4 min read

Tutorial: Learn Erlang from Scratch (2026)

Tutorial: Learn Erlang from Scratch (2026)

Published:  |  Category: Programming  |  Reading time: ~15 min
Tutorial: Learn Erlang from Scratch (2026)

Erlang was designed by Ericsson for building fault-tolerant, distributed telecommunications systems. After years building real-time messaging systems and multiplayer game servers, I understand why Erlang remains unmatched for concurrent, highly available systems. The actor model is baked into the language and the runtime.

Erlang's philosophy is simple: let processes crash and let supervisors restart them. This crash-first mentality leads to self-healing systems that achieve 99.999% uptime.

Concurrency

Erlang processes are isolated, lightweight actors. Each process has its own heap, communicating via message passing. spawn creates a process. ! sends a message. receive waits for messages with pattern matching. Processes are not OS threads — they are managed by BEAM (the Erlang VM) with no shared memory.

-module(counter).
-export([start/0, loop/0]).

loop() ->
  receive
    {inc, Pid} ->
      Pid ! {ok, inc},
      loop();
    {get, Pid} ->
      Pid ! {count, 0},
      loop();
    stop ->
      ok
  end.

start() ->
  Pid = spawn(fun loop/0),
  Pid ! {inc, self()},
  Pid ! {get, self()},
  receive {count, C} -> io:format("Count: ~p~n", [C]) end.

OTP

OTP (Open Telecom Platform) is Erlang's framework for building robust systems. GenServer is the core abstraction: a server process that handles calls (request-reply), casts (fire-and-forget), and info (raw messages). Supervisors manage process lifecycles with restart strategies (one_for_one, one_for_all, rest_for_one).

-module(my_server).
-behaviour(gen_server).

-export([start_link/0, get/0, increment/0]).
-export([init/1, handle_call/3, handle_cast/2]).

start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

init([]) -> {ok, 0}.

handle_call(get, _From, State) ->
  {reply, State, State};

handle_call(increment, _From, State) ->
  {reply, ok, State + 1}.

handle_cast(_Msg, State) ->
  {noreply, State}.

get() -> gen_server:call(?MODULE, get).
increment() -> gen_server:call(?MODULE, increment).

Pattern Matching

Pattern matching in Erlang is used for assignment (the = operator is actually a match operator), function clauses, case expressions, and receive blocks. Matching binds variables and destructures tuples, lists, and records. The match operator will fail if the sides do not unify — this is used intentionally for assertion.

% Assignment as matching
X = 42.
{X, Y} = {1, 2}.
[H|T] = [1, 2, 3].
% H=1, T=[2,3]

% Function clauses
factorial(0) -> 1;
factorial(N) when N > 0 -> N * factorial(N - 1).

% Case expression
case File of
  {ok, Data} -> process(Data);
  {error, Reason} -> log_error(Reason)
end.

% Guards
max(A, B) when A > B -> A;
max(_, B) -> B.

Fault Tolerance

Erlang's fault tolerance comes from the "let it crash" philosophy. Processes link together: if one dies, linked processes receive an exit signal. spawn_link creates a linked process. process_flag(trap_exit, true) converts exit signals to messages. Supervisors automatically restart crashed children, creating self-healing systems.

-module(fault_tolerant).
-export([start/0, worker/0]).

worker() ->
  process_flag(trap_exit, true),
  spawn_link(fun dangerous_work/0),
  receive
    {'EXIT', _Pid, _Reason} ->
      io:format("Worker crashed, restarting...~n"),
      worker()
  end.

dangerous_work() ->
  case maybe_fail() of
    ok -> dangerous_work();
    error -> exit(worker_error)
  end.

start() -> worker().

Hot Code Reloading

Erlang supports upgrading code without stopping the system. The BEAM keeps two versions of a module: the current and the old. code:load_file(Module) loads a new version. Running processes continue with the old version; new calls use the new version. OTP's release handler automates this with application upgrades.

% Load new version at runtime
code:load_file(my_module).

% Check versions
code:which(my_module).
% /path/to/my_module.beam

% OTP release upgrade
% relup file describes the upgrade path
% release_handler:install_release("2.0.0").

% Process code_change callback
-module(stateful_server).
-behaviour(gen_server).

-export([code_change/3]).

code_change({down, _OldVsn}, State, _Extra) ->
  {ok, transform_old(State)};
code_change(_OldVsn, State, _Extra) ->
  {ok, transform_new(State)}.

Distributed Systems

Erlang nodes connect via TCP/IP using net_adm. Connected nodes can spawn processes on remote nodes, send messages across nodes, and link across node boundaries. The global name registry provides cluster-wide process registration. The pg module handles process groups for publish-subscribe.

% Start a node with long names
% erl -name node1@192.168.1.10 -setcookie secret
% erl -name node2@192.168.1.11 -setcookie secret

% Connect
net_adm:ping('node2@192.168.1.11').

% Remote spawn
Pid = spawn('node2@192.168.1.11', fun worker:start/0).

% Global registration
global:register_name(my_service, Pid).
{ok, RemotePid} = global:whereis_name(my_service).

% RPC
rpc:call('node2@192.168.1.11', io, format, ["remote~n"]).

Frequently Asked Questions

Erlang vs Elixir?

Elixir runs on the BEAM and compiles to Erlang bytecode. It adds Ruby-like syntax, macros, and protocols. Erlang is mature; Elixir is more approachable with better tooling.

What is BEAM?

BEAM (Bogdan's Erlang Abstract Machine) is the Erlang VM. It manages processes, scheduling, memory, and distribution. Processes are lightweight with microsecond spawning time.

Does Erlang have shared memory?

No. All state is isolated per process. Communication is through message passing. This eliminates locks, mutexes, and race conditions.

What does the ! operator do?

Pid ! Message sends a message to the process identified by Pid. Messages are stored in the process's mailbox and processed sequentially.

Originally published on Ayodhyyya. Last updated June 1, 2026.