Skip to content

Erlang Cheatsheet

Concurrency-oriented functional language on the BEAM VM, built for fault-tolerant distributed systems.

01

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).

erlang
% 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.

erlang
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 discards

Tuples & 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.

erlang
% 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 tuple

Lists & 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.

erlang
[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 product

Functions & 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.

erlang
-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.

erlang
% 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.
02

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 =.

erlang
% 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.

erlang
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.

erlang
% 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.

erlang
% 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.
03

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.

erlang
% 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.

erlang
% 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 | undefined

receive — 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.

erlang
% 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.

erlang
% 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.

erlang
% 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.

erlang
% 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.
05

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.

erlang
-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.

erlang
% 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.

erlang
% 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.

erlang
% 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)'

Was this helpful?