Erlang Quick Reference for Experienced Programmers

For more see:

Language Basics

Syntax Essentials

% This is a comment

% Statements end with a period (.)
% Expressions within a function are separated by commas (,)
% Clauses (in case, if, function heads) are separated by semicolons (;)

% Variables start with Uppercase
Name = "Tom".

% Atoms start with lowercase (like Ruby symbols)
ok.
error.
hello_world.
'atoms with spaces'.

% Everything is an expression — last expression in a function is the return value

Data Types

% Atoms
ok.
error.
true.                          % Booleans are atoms
false.

% Numbers
42.                            % Integer
3.14.                          % Float
16#FF.                         % Base-16 (255)
2#1010.                        % Base-2 (10)

% Strings (are actually lists of integers)
"hello".                       % [104, 101, 108, 108, 111]
[72, 101, 108, 108, 111].     % "Hello" — same thing

% Binaries (byte sequences — the efficient string)
<<"hello">>.                   % Binary string (preferred for text)
<<72, 101, 108, 108, 111>>.   % Same binary

% Tuples (fixed-size, contiguous memory)
{ok, "result"}.
{error, not_found}.
{user, "Tom", 30}.

% Lists (linked lists)
[1, 2, 3].
[Head | Tail] = [1, 2, 3].    % Head = 1, Tail = [2, 3]
[1 | [2 | [3 | []]]].         % How lists actually work

% Property lists (convention: list of {Key, Value} tuples)
[{name, "Tom"}, {age, 30}].
proplists:get_value(name, List).

% Maps (Erlang 17+)
#{name => "Tom", age => 30}.

% Records (compile-time syntactic sugar over tuples)
-record(user, {name, email, active = true}).
User = #user{name = "Tom", email = "tom@test.com"}.
User#user.name.                % Access field

Pattern Matching

% Variables are single-assignment — once bound, matching against them
X = 5.
X = 5.           % OK — matches
X = 6.           % ** badmatch error — X is already 5

% Tuple matching
{ok, Result} = {ok, 42}.          % Result = 42
{ok, Result} = {error, "fail"}.   % ** badmatch

% List matching
[H | T] = [1, 2, 3].              % H = 1, T = [2, 3]
[_, Second | _] = [a, b, c, d].   % Second = b, _ = don't care

% Map matching
#{name := Name} = #{name => "Tom", age => 30}.   % Name = "Tom"

% In function heads
area({circle, Radius}) -> math:pi() * Radius * Radius;
area({rectangle, W, H}) -> W * H;
area({square, Side}) -> Side * Side.

% In case expressions
case File:read("data.txt") of
    {ok, Contents} -> process(Contents);
    {error, enoent} -> io:format("File not found~n");
    {error, Reason} -> io:format("Error: ~p~n", [Reason])
end.

Functions

Module Functions

-module(math_utils).
-export([add/2, divide/2]).     % Public functions (name/arity)

add(A, B) -> A + B.

divide(_, 0) -> {error, division_by_zero};
divide(A, B) -> {ok, A / B}.

Function Clauses & Guards

-module(greeting).
-export([greet/1]).

greet(Name) when is_binary(Name) -> <<"Hello, ", Name/binary>>;
greet(Name) when is_list(Name) -> "Hello, " ++ Name;
greet(Name) when is_atom(Name) -> greet(atom_to_list(Name)).

Common Guards

is_integer(X)
is_float(X)
is_number(X)
is_atom(X)
is_binary(X)       % Binary/string
is_list(X)
is_tuple(X)
is_map(X)
is_pid(X)
is_boolean(X)
X > 0
X >= 0 andalso X =< 100    % andalso/orelse (short-circuit)
length(List) > 0
map_size(Map) > 0

Anonymous Functions (funs)

Add = fun(A, B) -> A + B end.
Add(1, 2).                        % => 3

Double = fun(X) -> X * 2 end.
lists:map(Double, [1, 2, 3]).     % => [2, 4, 6]

% Inline
lists:filter(fun(X) -> X > 2 end, [1, 2, 3, 4]).  % => [3, 4]

% Capture module function
lists:map(fun erlang:abs/1, [-1, -2, 3]).  % => [1, 2, 3]

% Multi-clause fun
F = fun
    (0) -> zero;
    (N) when N > 0 -> positive;
    (_) -> negative
end.

Modules

-module(user).                     % Module name (must match filename)
-export([new/2, full_name/1]).     % Public API
-export_type([t/0]).               % Export types

-type t() :: #{
    name := binary(),
    email := binary(),
    active := boolean()
}.

-spec new(binary(), binary()) -> t().
new(Name, Email) ->
    #{name => Name, email => Email, active => true}.

-spec full_name(t()) -> binary().
full_name(#{name := Name}) -> Name.

Module Attributes

-module(config).
-author("Tom").
-vsn("1.0.0").

-define(MAX_RETRIES, 3).          % Macro (compile-time constant)
-define(LOG(Msg), io:format("~p: ~p~n", [?MODULE, Msg])).

retry(F) -> retry(F, ?MAX_RETRIES).
retry(_, 0) -> {error, max_retries};
retry(F, N) ->
    case F() of
        {ok, Result} -> {ok, Result};
        {error, _} -> retry(F, N - 1)
    end.

Control Flow

% case (pattern matching)
case lists:keyfind(name, 1, Props) of
    {name, Value} -> Value;
    false -> undefined
end.

% if (guard-based — no pattern matching, rarely used)
if
    X > 0 -> positive;
    X < 0 -> negative;
    true -> zero               % 'true' is the else clause
end.

% receive (message passing)
receive
    {msg, Text} -> io:format("Got: ~s~n", [Text]);
    stop -> ok
after
    5000 -> timeout            % Timeout in milliseconds
end.

Why case Over if

if only supports guard expressions — no pattern matching. case is far more powerful and idiomatic. Most Erlang code uses case or function clause matching instead of if.

List Operations

% Common list functions
lists:map(fun(X) -> X * 2 end, [1, 2, 3]).          % => [2, 4, 6]
lists:filter(fun(X) -> X > 1 end, [1, 2, 3]).       % => [2, 3]
lists:foldl(fun(X, Acc) -> X + Acc end, 0, [1, 2, 3]).  % => 6
lists:foldr(fun(X, Acc) -> X + Acc end, 0, [1, 2, 3]).  % => 6
lists:any(fun(X) -> X > 2 end, [1, 2, 3]).          % => true
lists:all(fun(X) -> X > 0 end, [1, 2, 3]).          % => true
lists:member(2, [1, 2, 3]).                          % => true
lists:sort([3, 1, 2]).                               % => [1, 2, 3]
lists:reverse([1, 2, 3]).                            % => [3, 2, 1]
lists:flatten([[1, 2], [3, [4]]]).                   % => [1, 2, 3, 4]
lists:zip([1, 2], [a, b]).                           % => [{1, a}, {2, b}]
lists:nth(2, [a, b, c]).                             % => b (1-indexed)
lists:seq(1, 5).                                     % => [1, 2, 3, 4, 5]
length([1, 2, 3]).                                   % => 3

% List comprehensions
[X * 2 || X <- [1, 2, 3]].                          % => [2, 4, 6]
[X || X <- [1, 2, 3, 4], X rem 2 == 0].             % => [2, 4]
[{X, Y} || X <- [1, 2], Y <- [a, b]].               % => [{1,a},{1,b},{2,a},{2,b}]

% Cons operator
[1 | [2, 3]].                                        % => [1, 2, 3]

% String operations (strings are lists)
string:uppercase("hello").                            % => "HELLO"
string:split("a,b,c", ",").                          % => ["a", "b", "c"]
string:trim("  hello  ").                            % => "hello"

Maps (Erlang 17+)

% Create
Map = #{name => "Tom", age => 30}.

% Access
maps:get(name, Map).                  % => "Tom"
maps:get(missing, Map, default).      % => default
maps:find(name, Map).                 % => {ok, "Tom"}

% Update (key must exist for :=)
Map2 = Map#{age := 31}.              % Update existing key
Map3 = Map#{email => "tom@t.com"}.   % Add new key with =>

% Operations
maps:keys(Map).                       % => [age, name]
maps:values(Map).                     % => [30, "Tom"]
maps:merge(Map1, Map2).              % Merge (Map2 wins on conflict)
maps:remove(age, Map).               % Remove key
maps:is_key(name, Map).              % => true
maps:size(Map).                       % => 2
maps:to_list(Map).                    % => [{age, 30}, {name, "Tom"}]
maps:from_list([{a, 1}]).            % => #{a => 1}

% Pattern matching with maps
#{name := Name} = Map.               % Name = "Tom"

handle_request(#{method := get, path := Path}) ->
    serve_page(Path);
handle_request(#{method := post, body := Body}) ->
    process_body(Body).

Processes & Concurrency

% Spawn a process
Pid = spawn(fun() -> io:format("Hello from process~n") end).

% Spawn with module/function/args
Pid = spawn(module, function, [Arg1, Arg2]).

% Send message
Pid ! {self(), hello}.

% Receive message
receive
    {From, Msg} ->
        From ! {self(), received},
        io:format("Got: ~p~n", [Msg])
after
    5000 -> timeout
end.

% Self
self().                               % Current process PID

% Links (bidirectional — crash together)
link(Pid).
spawn_link(fun() -> error(boom) end).

% Monitors (unidirectional — get notified, don't crash)
Ref = monitor(process, Pid).
receive
    {'DOWN', Ref, process, Pid, Reason} ->
        io:format("Process died: ~p~n", [Reason])
end.

% Register process by name
register(my_server, Pid).
my_server ! {self(), hello}.          % Send by name

% Process info
is_process_alive(Pid).
process_info(Pid).

Simple Client-Server Pattern

-module(counter).
-export([start/0, increment/1, get/1]).

start() ->
    spawn(fun() -> loop(0) end).

increment(Pid) ->
    Pid ! increment.

get(Pid) ->
    Pid ! {get, self()},
    receive
        {count, N} -> N
    after
        1000 -> {error, timeout}
    end.

loop(Count) ->
    receive
        increment ->
            loop(Count + 1);
        {get, From} ->
            From ! {count, Count},
            loop(Count)
    end.

OTP: gen_server

-module(counter).
-behaviour(gen_server).

%% API
-export([start_link/0, increment/0, get_count/0]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2]).

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

increment() ->
    gen_server:cast(?MODULE, increment).

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

%%% Callbacks %%%
init(Initial) ->
    {ok, Initial}.

handle_cast(increment, Count) ->
    {noreply, Count + 1}.

handle_call(get, _From, Count) ->
    {reply, Count, Count}.

gen_server Callback Return Values

Callback Return Meaning
init/1 {ok, State} Start with State
init/1 {stop, Reason} Don't start
handle_call/3 {reply, Reply, NewState} Send Reply to caller
handle_call/3 {noreply, NewState} Don't reply yet
handle_cast/2 {noreply, NewState} Continue
handle_cast/2 {stop, Reason, NewState} Shutdown

OTP: Supervisors

-module(my_sup).
-behaviour(supervisor).

-export([start_link/0, init/1]).

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

init([]) ->
    SupFlags = #{
        strategy => one_for_one,
        intensity => 5,           % Max 5 restarts
        period => 60              % Per 60 seconds
    },
    Children = [
        #{
            id => counter,
            start => {counter, start_link, []},
            restart => permanent,
            type => worker
        },
        #{
            id => logger,
            start => {my_logger, start_link, []},
            restart => transient,    % Only restart on abnormal exit
            type => worker
        }
    ],
    {ok, {SupFlags, Children}}.

Supervision Strategies

Strategy Behavior
one_for_one Only restart the failed child
one_for_all Restart all children if one fails
rest_for_one Restart the failed child and all started after it
simple_one_for_one Dynamic children, all same spec (deprecated — use simple_one_for_one or DynamicSupervisor in Elixir)

Restart Types

Type When to Restart
permanent Always restart (default)
transient Only restart on abnormal exit
temporary Never restart

Error Handling

% Pattern matching on tagged tuples (preferred)
case file:read("data.txt") of
    {ok, Contents} -> process(Contents);
    {error, enoent} -> io:format("Not found~n");
    {error, Reason} -> io:format("Error: ~p~n", [Reason])
end.

% try/catch
try
    risky_operation()
catch
    throw:Reason -> io:format("Thrown: ~p~n", [Reason]);
    error:Reason -> io:format("Error: ~p~n", [Reason]);
    exit:Reason -> io:format("Exit: ~p~n", [Reason])
after
    cleanup()
end.

% Three kinds of exceptions
throw(my_reason).          % Deliberate non-local return (catchable)
error(bad_argument).       % Runtime error (usually a bug)
exit(shutdown).            % Process exit signal

% catch expression (older style — prefer try/catch)
Result = (catch some_function()).  % Returns {'EXIT', Reason} on error

Exception Types

Type Meaning Raised By
throw Expected, deliberate, non-local return throw(Reason)
error Runtime errors, bugs error(Reason), badmatch, badarith, etc.
exit Process termination signal exit(Reason)

ETS (Erlang Term Storage)

% In-memory key-value storage shared between processes
Tab = ets:new(my_table, [set, named_table, public]).

ets:insert(my_table, {key, "value"}).
ets:lookup(my_table, key).            % => [{key, "value"}]
ets:delete(my_table, key).
ets:match(my_table, {'$1', "value"}). % => [[key]]

% Table types
% set       — unique keys (default)
% ordered_set — unique keys, sorted
% bag       — duplicate keys allowed, unique {key, value}
% duplicate_bag — fully duplicate entries allowed

Binary & Bit Syntax

% Binaries are byte sequences
Bin = <<"hello">>.
<<H, Rest/binary>> = <<"hello">>.    % H = 104, Rest = <<"ello">>

% Bit syntax for protocol parsing
<<Version:4, Type:4, Length:16, Payload/binary>> = Packet.

% Construct binaries
<<1:8, 256:16, "data"/binary>>.

% Binary comprehensions
<< <<(X*2)>> || <<X>> <= <<1, 2, 3>> >>.  % => <<2, 4, 6>>

Common Built-in Functions (BIFs)

% Type conversions
list_to_binary("hello").              % => <<"hello">>
binary_to_list(<<"hello">>).          % => "hello"
integer_to_list(42).                  % => "42"
list_to_integer("42").                % => 42
atom_to_list(hello).                  % => "hello"
list_to_atom("hello").                % => hello
term_to_binary(Any).                  % Serialize any term
binary_to_term(Bin).                  % Deserialize

% Tuple operations
tuple_size({a, b, c}).                % => 3
element(2, {a, b, c}).                % => b (1-indexed)
setelement(2, {a, b, c}, x).         % => {a, x, c}

% I/O
io:format("Hello ~s, you are ~p years old~n", ["Tom", 30]).
% ~s = string, ~p = pretty-print any term, ~w = write, ~n = newline
% ~B = integer (base 10), ~.2f = float with 2 decimals

The Shell (erl)

% Start
erl                                    % Start Erlang shell
erl -pa ebin                           % Add path to code search
erl -name node@host                    % Start distributed node

% Shell commands
help().                                % Show help
q().                                   % Quit
c(module).                             % Compile module
l(module).                             % Load/reload module
m(module).                             % Module info
f().                                   % Forget all bindings
f(X).                                  % Forget binding for X
rr(module).                            % Read record definitions

Build Tools

# rebar3 (standard build tool)
rebar3 new app myapp                   # Create new application
rebar3 new release myrelease           # Create new release project
rebar3 compile                         # Compile
rebar3 eunit                           # Run EUnit tests
rebar3 ct                              # Run Common Test suites
rebar3 shell                           # Start shell with project loaded
rebar3 dialyzer                        # Static type analysis
rebar3 release                         # Build release

# erlc (compiler)
erlc module.erl                        # Compile single module
erlc -o ebin src/*.erl                 # Compile to output directory

Dependencies (rebar.config)

% rebar.config
{erl_opts, [debug_info]}.

{deps, [
    {cowboy, "2.10.0"},                % Hex package
    {jsx, {git, "https://github.com/talentdeficit/jsx.git", {tag, "v3.1.0"}}}
]}.

{profiles, [
    {test, [
        {deps, [{meck, "0.9.2"}]}     % Test-only dependency
    ]}
]}.

Key Differences from Other Languages

That's the essential Erlang. The rest is mastering OTP patterns, understanding the BEAM's scheduling and memory model, and exploring the extensive standard library.