Basics
Hello World & Modules
Every Erlang file is a module declared with -module(name). Exported functions are listed with -export([name/arity]). arity (number of args) is part of the function identity — greet/0 and greet/1 are different functions. Compile from the shell with c(Module).
% hello.erl
-module(hello).
-export([greet/1, greet/0]).
greet() ->
greet("World").
greet(Name) ->
io:format("Hello, ~s!~n", [Name]).
% Compile & run in the shell:
% c(hello).
% hello:greet("Alice"). %=> Hello, Alice!Variables & Atoms
Variables start with an uppercase letter (or _) and are single-assignment — once bound they cannot change. Re-matching a bound variable is actually an assertion: X = X+1 raises a badmatch error. Atoms are named constants starting with lowercase; they're stored in a global table and compared by identity.
Name = "Alice". % variables start with uppercase
Age = 30. % bound once — single assignment!
% Age = 31. % ** exception error: no match of RHS
% Atoms start with lowercase (or are quoted)
ok. % the ok atom (common return value)
error. % another atom
'This is an atom'. % quoted atom with spaces
true, false, undefined % built-in atoms
% Pattern matching binds and asserts
{ok, Result} = {ok, 42}. % Result = 42
{error, _} = {error, badarg}. % _ matches and discardsTuples & Records
Tuples are fixed-size containers written {a,b,c}. Records are tuples with named fields, declared with -record. Updating a record field creates a new record (immutable data). The first element is conventionally a tag atom (like point or person) for clearer pattern matching.
% Tuples (fixed-size, positional)
Point = {point, 3, 4}.
{point, X, Y} = Point. % X=3, Y=4
element(2, Point). %=> point
% Records (syntactic sugar over tuples)
-record(person, {name, age, city = "Unknown"}).
P = #person{name = "Alice", age = 30}.
P#person.name. %=> "Alice"
P2 = P#person{age = 31}. % update field
{person, Name, Age, _} = P. % underlying tupleLists & List Comprehensions
Lists are singly-linked lists built with [Head|Tail]. ++ concatenates (O(n) on left), -- removes elements. List comprehensions [Expr || Qualifier1, Qualifier2, ...] support generators (X <- List) and filters (boolean expressions). They are the idiomatic way to transform lists.
[1,2,3] ++ [4,5]. %=> [1,2,3,4,5] (append)
[1,2,3] -- [2]. %=> [1,3] (subtract)
hd([1,2,3]). %=> 1 (head)
tl([1,2,3]). %=> [2,3] (tail)
% Pattern matching on lists
[Head | Tail] = [1,2,3]. % Head=1, Tail=[2,3]
[A,B|Rest] = [1,2,3,4]. % A=1, B=2, Rest=[3,4]
% List comprehensions
[ X*2 || X <- [1,2,3,4] ]. %=> [2,4,6,8]
[ X || X <- [1..10], X rem 2 =:= 0 ]. % evens: [2,4,6,8,10]
[ {A,B} || A <- [1,2], B <- [a,b] ]. % cartesian productFunctions & Pattern Matching
Functions are defined as multiple clauses separated by ;, terminated by a period. Clauses are tried in order; the first whose pattern AND guard succeed is used. Tagged tuples like {ok, Val} / {error, Reason} are the standard way to signal success/failure without exceptions.
-module(math_tools).
-export([factorial/1, fib/1, classify/1]).
% Multiple clauses selected by pattern matching
factorial(0) -> 1;
factorial(N) when N > 0 -> N * factorial(N - 1).
% Fibonacci with guards
fib(0) -> 0;
fib(1) -> 1;
fib(N) when N > 1 -> fib(N-1) + fib(N-2).
% Tagged returns (common Erlang idiom)
classify(X) when X < 0 -> {negative, X};
classify(0) -> zero;
classify(X) when X > 0 -> {positive, X}.Maps
Maps (since R17) are key-value dictionaries. Use => to set/insert (creates if absent) and := to update (key must already exist). In pattern matching only := is allowed. maps:get/2 raises badarg on missing keys; use maps:get/3 for a default or maps:find/2 for an {ok,_}|error tuple.
% Create
M = #{ name => "Alice", age => 30, role => admin }.
% Lookup
maps:get(name, M). %=> "Alice"
maps:get(city, M, "Unknown"). % default if missing
{ok, V} = maps:find(age, M). %=> {ok, 30}
% Update / add (immutable — returns new map)
M2 = M#{ age => 31, city => "Paris" }.
% Pattern match on maps
#{ name := N } = M. % N = "Alice" (:= for matching, => for setting)
#{ age := A, role := R } = M.Pattern Matching & Guards
Pattern Matching in Function Heads
Erlang selects a function clause by pattern matching the arguments top-down. Patterns can bind new variables, assert equality against literals or previously bound variables, and ignore fields with _. The same mechanism is used for receive, case, and =.
% Select clause by structure of argument
handle({ping, From}) -> From ! pong;
handle({echo, Msg}) -> Msg;
handle({add, A, B}) -> A + B;
handle(stop) -> bye.
% Tuple destructuring
-record(rect, {w, h}).
area(#rect{w=W, h=H}) -> W * H.
% List patterns with literal values
greet_first([H|_]) -> io:format("Hi ~p~n", [H]);
greet_first([]) -> ok.Case Expressions
case evaluates an expression and matches it against a sequence of patterns. Each clause may have a guard. It's the standard control-flow tool when a function's branching depends on a computed value rather than the original arguments. Always include a catch-all _ clause unless you want a crash on unexpected input.
classify_file(Size) ->
case Size of
0 -> empty;
N when N < 1024 -> small;
N when N < 1048576 -> medium;
_ -> large
end.
% Matching on tagged tuples
Result = case file:read_file("a.txt") of
{ok, Bin} -> {ok, binary_to_list(Bin)};
{error, enoent} -> {error, missing};
{error, Reason} -> {error, Reason}
end.Guards
Guards restrict when a clause matches. They are limited to a known set of built-in functions so they always terminate and have no side effects. Comma = AND, semicolon = OR. Guards are re-evaluated when used in a guard sequence, so prefer pattern matching over guards where possible.
% Guards go after 'when' and may use built-in guard tests (BIFs)
max(A, B) when A >= B -> A;
max(_, B) -> B.
% Multiple guard tests: ',' = AND, ';' = OR
classify(N) when N > 0, N < 100 -> in_range;
classify(N) when N =< 0; N >= 100 -> out_of_range.
% Allowed guard BIFs include comparison, arithmetic, type tests:
% is_integer/1, is_atom/1, is_list/1, is_map/1,
% length/1, map_size/1, abs/1, element/2, hd/1, tl/1
% (No user functions — guards must be side-effect free and terminate!)Bit Syntax & Binaries
The bit syntax << Seg1, Seg2, ... >> packs/unpacks binaries by bit fields. Each segment is Value:Size/TypeSpecifier where Type is integer|binary|float|bits, with optional sign and endianness. This makes Erlang exceptionally good at parsing network protocols and file formats.
% Pack a 16-bit length + 8-bit type + payload
Bin = << Length:16, Type:8, Payload/binary >>.
% Unpack with the same pattern
<< L:16, T:8, Rest/binary >> = Bin.
% Bit fields & endianness
<< N:32/big-unsigned-integer >> = <<0,0,1,1>>. % N = 261
<< N:32/little-signed-integer >> = <<1,0,0,0>>. % N = 1
% IPv4 header parsing example
<< Version:4, IHL:4, _TOS:8, TotalLen:16,
_/binary >> = Packet.Concurrent Programming
spawn — Start a Process
spawn(M,F,A) creates a new BEAM process — a lightweight green thread with its own heap and mailbox. Processes share no memory and communicate only by message passing. BEAM can run tens of millions of processes; each is ~2-3 KB. spawn/1 takes a fun and is convenient for ad-hoc workers.
% spawn/3 starts a new lightweight process
Pid = spawn(Module, Function, ArgsList).
Pid = spawn(fun() -> timer:sleep(1000), io:format("done~n") end).
% Every process has a PID; processes share NO memory
% (BEAM processes are NOT OS threads — millions are possible)
% self() returns the current process's PID
io:format("I am ~p~n", [self()]).
% is_process_alive/1 checks liveness
case is_process_alive(Pid) of
true -> ok;
false -> {error, dead}
end.send (!) — Message Passing
Pid ! Msg asynchronously sends a message to the recipient's mailbox. Delivery is guaranteed and order is preserved between the same pair of processes. ! returns the message, enabling broadcast lists. Processes can be registered under an atom name so any code can send to them without holding the PID.
% Send a message: Pid ! Message
Pid ! {hello, self()}.
Pid ! ping.
Pid ! {compute, 1, 2, 3}.
% ! always returns the message itself, so you can broadcast:
[ P ! broadcast || P <- AllPids ].
% Send to a registered process (by atom name)
registered_name ! {request, Ref, self()}.
% Register / unregister a name for a PID
register(server, Pid).
unregister(server).
whereis(server). %=> Pid | undefinedreceive — Pattern-Match Messages
receive scans the mailbox in order for the first message matching any clause; if none match it suspends until one arrives. The after clause sets a timeout (0 = non-blocking check, infinity = forever). A common server pattern is a tail-recursive loop() that calls itself after each message.
% receive blocks until a matching message arrives
loop() ->
receive
{ping, From} ->
From ! pong,
loop();
{echo, Msg} ->
io:format("echo: ~p~n", [Msg]),
loop();
stop ->
ok
end.
% receive with a timeout
receive
{data, X} -> process(X)
after 5000 ->
{error, timeout}
end.
% Flush mailbox (drain all messages)
flush() ->
receive _ -> flush() after 0 -> ok end.Selective & Non-Selective Receive
receive is selective: it scans for the first matching message even if older messages exist. This is great for matching a specific reply (using a unique reference Ref) but stale non-matching messages accumulate and slow scans. gen_server avoids this by handling every message in order. Use make_ref() to create unique references for request/reply correlation.
% Selective receive: skips non-matching messages
get_reply(Ref) ->
receive
{Ref, Reply} -> Reply % only matches the reply with our Ref
after 5000 ->
{error, timeout}
end.
% Problem: other messages stay in the mailbox, slowing future receives.
% For high-throughput servers use a selective receive library or
% gen_server (which keeps its mailbox clean).
% Non-selective (drain in order):
drain_all() ->
receive
Msg -> [Msg | drain_all()]
after 0 ->
[]
end.Request / Reply with References
The call pattern pairs each request with a unique reference (make_ref/0). The client sends {call, Ref, self(), Request} and waits for a reply tagged with the same Ref — this lets the client pick out its reply from any other traffic in its mailbox. This is the foundation of Erlang's RPC and of gen_server:call.
% Client: send a tagged request, await the matching reply
call(Server, Request) ->
Ref = make_ref(),
Server ! {call, Ref, self(), Request},
receive
{Ref, Reply} -> Reply
after 5000 ->
exit(timeout)
end.
% Server side:
handle({call, Ref, From, Request}, State) ->
Reply = do_work(Request),
From ! {Ref, Reply},
State.Tail Recursion & Server Loop
BEAM processes hold state via tail recursion: the loop calls itself with the new state as the last expression, so it doesn't grow the stack. This is the canonical stateful-server pattern — every receive clause ends with a tail call to loop/1 with the updated state. gen_server formalises exactly this pattern.
% A counter server — each message mutates state via recursion
start_counter(Init) -> spawn(fun() -> counter_loop(Init) end).
counter_loop(N) ->
receive
inc -> counter_loop(N + 1);
dec -> counter_loop(N - 1);
{get, From} -> From ! {value, N}, counter_loop(N);
reset -> counter_loop(0)
end.
% Caller:
Pid = start_counter(0).
Pid ! inc.
Pid ! {get, self()}.
receive {value, V} -> io:format("count = ~p~n", [V]) end.Links, Monitors & Errors
link / unlink — Bidirectional Link
link/1 creates a bidirectional link: if either process exits, the other receives an exit signal. By default that signal also crashes the receiver. Setting process_flag(trap_exit, true) converts signals into {'EXIT', From, Reason} messages, which is how supervisors detect child failures. spawn_link/1 does both atomically to avoid a race window.
% link/1 links current process to another
Pid = spawn(fun() -> ... end),
link(Pid).
% If either dies, the other gets an exit signal.
% Default behavior: the linked partner also exits.
% spawn_link does both atomically:
Pid = spawn_link(fun() -> ... end).
% Trap exits to convert death signals into messages:
process_flag(trap_exit, true).
receive
{'EXIT', From, Reason} -> io:format("~p died: ~p~n", [From, Reason])
end.
unlink(Pid). % remove the link (best-effort)monitor / demonitor — One-Way Monitor
monitor(process, Pid) is one-way: only the caller gets a 'DOWN' message, and the monitored process is unaffected. This is preferable to link when you only want to observe — e.g. a call/1 implementation that needs to know if the server died during the call. Each monitor returns a unique Ref to distinguish multiple monitors.
% monitor/1: one-way — we observe Pid, but it doesn't observe us
Ref = monitor(process, Pid).
% If Pid dies, we receive:
% {'DOWN', Ref, process, Pid, Reason}
receive
{'DOWN', Ref, process, Pid, normal} -> ok;
{'DOWN', Ref, process, Pid, Reason} -> {error, Reason}
end.
% demonitor/1 removes the monitor
demonitor(Ref).
% demonitor/2 with [flush] also drains any pending 'DOWN' message
demonitor(Ref, [flush]).exit Signals & Reasons
exit(Reason) terminates the current process; exit(Pid, Reason) sends an exit signal to Pid. Reason matters: normal and shutdown are non-propagating (linked partners survive), other reasons propagate and kill linked partners unless they trap exits. The kill reason is untrappable — use it as a last resort to terminate a stuck process.
% Send an exit signal explicitly:
exit(Pid, Reason). % tell Pid to exit with Reason
% Terminate the current process:
exit(Reason). % Reason != normal kills linked partners
% exit/2 with the atom 'kill' is untrappable:
exit(Pid, kill). % Pid dies even if it traps exits.
% (Only the kernel-supervised 'kill' reason cannot be trapped.)
% Reasons:
% normal -> linked partners do NOT die
% shutdown -> linked partners do NOT die (used in graceful stop)
% {shutdown, Term} -> graceful with reason, also non-propagating
% anything else -> linked partners also die (unless trapping)Try / Catch & Exceptions
try ... catch Class:Reason handles exceptions of three classes: throw (explicit throw/1, used for non-local return), error (runtime errors like badarg, or error/1), and exit (exit/1). The after clause runs regardless and is for cleanup. erlang:get_stacktrace() captures the stack trace from within the matching clause.
try
{ok, Bin} = file:read_file(Path),
binary_to_list(Bin)
catch
throw:Term -> {thrown, Term};
error:Reason -> {error, Reason, erlang:get_stacktrace()};
exit:Reason -> {exit, Reason}
after
% always runs (cleanup), regardless of success/failure
file:close(Fd)
end.
% Raise exceptions:
throw({bad, Value}). % catchable as throw
erlang:error(badarg). % catchable as error
exit(timeout). % catchable as exitSupervisor Pattern (Manual)
A supervisor traps exits, starts its child with spawn_link, and on receiving {'EXIT', Pid, _} restarts the child. This 'let it crash' philosophy — keep the supervisor simple and let workers fail fast — is the heart of Erlang's fault tolerance. In practice you use the OTP supervisor behaviour instead of rolling your own.
% A minimal supervisor: start a child, restart on death
start_sup(Mod) ->
process_flag(trap_exit, true),
Pid = spawn_link(fun() -> Mod:run() end),
sup_loop(Mod, Pid).
sup_loop(Mod, Pid) ->
receive
{'EXIT', Pid, Reason} ->
io:format("child died ~p, restarting~n", [Reason]),
NewPid = spawn_link(fun() -> Mod:run() end),
sup_loop(Mod, NewPid);
{stop, From} ->
exit(Pid, shutdown),
From ! ok
end.OTP gen_server (Brief)
gen_server Behaviour Skeleton
Implementing -behaviour(gen_server) tells the compiler to check that all required callback functions exist. The server state flows through init/1 -> handle_call/3 (synchronous) / handle_cast/2 (asynchronous) -> terminate/2. ?MODULE expands to the current module name; the {local, Name} registration lets clients call by atom.
-module(counter).
-behaviour(gen_server).
-export([start_link/0, inc/0, get/0]).
-export([init/1, handle_call/3, handle_cast/2,
handle_info/2, terminate/2, code_change/3]).
start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, 0, []).
inc() -> gen_server:cast(?MODULE, inc).
get() -> gen_server:call(?MODULE, get).
init(N) -> {ok, N}.
handle_call(get, _F, N) -> {reply, N, N}.
handle_cast(inc, N) -> {noreply, N + 1}.
handle_info(_, N) -> {noreply, N}.
terminate(_, _) -> ok.
code_change(_, N, _) -> {ok, N}.Call vs Cast
call is synchronous and blocks the caller until the server replies (or until the default 5s timeout, after which the caller exits). cast is asynchronous and always returns ok immediately. call is built on the request/reply-with-reference pattern — it also monitors the server so a crash surfaces as an exit in the caller.
% Synchronous call — waits for reply, crashes on server death
{ok, Value} = gen_server:call(Server, {get, Key}).
% Asynchronous cast — fire and forget
ok = gen_server:cast(Server, {put, Key, Value}).
% Call supports timeout and monitor under the hood:
gen_server:call(Server, Req, 5000).
% Server replies in handle_call:
handle_call({get, Key}, From, State) ->
{reply, maps:get(Key, State), State}.
% handle_cast never replies:
handle_cast({put, K, V}, State) ->
{noreply, maps:put(K, V, State)}.handle_info & Misc Callbacks
handle_info/2 catches all non-call/cast messages — anything sent with Pid ! Msg, plus system messages like 'DOWN', timeouts, and socket data. terminate/2 is the place to release resources; it is NOT guaranteed to run on a brutal kill. code_change/3 powers Erlang's famous non-stop hot code upgrade.
% handle_info/2 handles messages NOT from call/cast:
% - plain messages from other processes (Pid ! Msg)
% - 'DOWN' messages from monitors
% - timeouts sent by gen_server:start(..., Timeout)
handle_info({'DOWN', Ref, process, Pid, Reason}, State) ->
io:format("monitored ~p died: ~p~n", [Pid, Reason]),
{noreply, State};
handle_info({tcp, Socket, Data}, State) ->
%% handle incoming socket data
{noreply, State};
handle_info(Info, State) ->
{noreply, State}.
% terminate/2 runs on shutdown — close fds, flush, etc.
terminate(Reason, State) ->
io:format("shutting down: ~p~n", [Reason]),
ok.
% code_change/3 enables hot code upgrade.
code_change(_OldVsn, State, _Extra) -> {ok, State}.Application Structure (OTP)
An OTP application is a self-contained component with a .app resource file describing its modules, dependencies, and entry module (mod). The application behaviour's start/2 returns the top-level supervisor PID, which in turn starts workers. Releases bundle applications into a runnable system — this is how Erlang ships non-stop, hot-upgradable services.
% myapp.app — application resource file
{application, myapp,
[{description, "Demo app"},
{vsn, "1.0.0"},
{modules, [myapp_app, myapp_sup, counter]},
{registered, [myapp_sup]},
{applications, [kernel, stdlib]},
{mod, {myapp_app, []}},
{env, [{port, 4000}]}
]}.
% myapp_app.erl — application behaviour
-module(myapp_app).
-behaviour(application).
-export([start/2, stop/1]).
start(_StartType, _StartArgs) -> myapp_sup:start_link().
stop(_State) -> ok.
% Start everything:
% application:start(myapp).
% Or with release: erl -eval 'application:ensure_all_started(myapp)'Related Erlang snippets
Copy-paste ready code for common tasks.
Ping-Pong Processes
Two processes pass messages back and forth using spawn and receive.
gen_server Counter
Build a stateful server with the gen_server behaviour.
Supervisor Tree
Define a supervisor that starts workers and restarts them on crash.
Selective Receive with References
Match a specific reply out of many messages using a unique reference.
Links, Exit Trapping & 'Let It Crash'
Use link and trap_exit to detect process death and recover.
Stateful Server via Tail Recursion
Hold mutable state in a process by threading it through recursive calls.
Parallel Map with rpc:pmap
Apply a function to each list element in parallel across processes.
Hot Code Upgrade
Reload a module's code without stopping the running system.
Was this helpful?