Coding · Programming languages
Erlang tutorial for beginners
Short answer
Erlang is a functional, concurrent language created at Ericsson for systems that must stay up, such as telephone switches and messaging servers. Programs run on the BEAM virtual machine as many lightweight processes that communicate by messages and are restarted by supervisors when they fail. This page covers compiling modules, types, pattern matching, recursion, lists and maps, processes, OTP and error handling.
What Erlang is and what it is used for
Erlang is a functional, dynamically typed language designed at Ericsson in the 1980s for telephone switches that had to run without stopping. It was released as open source in 1998. Erlang programs run on the BEAM virtual machine as many lightweight processes that share nothing and communicate by messages, and they come with OTP (Open Telecom Platform), a set of libraries and design principles for building fault-tolerant systems.
Erlang is used in telecom equipment, messaging platforms (WhatsApp's backend was famously built on Erlang), message brokers such as RabbitMQ, databases such as Riak and CouchDB, and payment and IoT systems. Elixir and Gleam also run on the BEAM and can use Erlang libraries directly.
Install Erlang and run Hello, world
Install Erlang/OTP from erlang.org or with a package manager (for example brew install erlang or apt install erlang). Code lives in modules; each file name.erl defines the module name and lists the functions it exports.
% hello.erl
-module(hello).
-export([main/0]).
main() ->
io:format("Hello, world!~n").erlc hello.erl # compiles to hello.beam
erl -noshell -s hello main -s init stop
# Hello, world!
erl # interactive shell; c(hello). then hello:main().Comments start with %. ~n in a format string is a newline and ~p prints any term. In the shell, every expression ends with a period.
Punctuation, variables and basic types
Erlang's punctuation is its biggest surprise for beginners: a comma separates expressions, a semicolon separates clauses, and a period ends a function or a shell command. Variables start with an uppercase letter or underscore and can be bound only once.
1> Name = "Erlang".
"Erlang"
2> Year = 1986, Next = Year + 1.
1987
3> Year = 2000.
** exception error: no match of right hand side value 2000
4> 10 / 4.
2.5
5> {10 div 4, 10 rem 4}.
{2,2}
6> is_atom(ok).
true
7> <<"binary string">>.
<<"binary string">>| Type | Example | Notes |
|---|---|---|
| integer | 42, 16#FF | arbitrary precision; 16# for hex |
| float | 3.14 | 64-bit double precision |
| atom | ok, error, 'Hello' | named constants, lowercase or quoted |
| tuple | {ok, 42} | fixed-size group of values |
| list | [1, 2, 3] | linked list; strings like "abc" are lists of integers |
| map | #{name => "Ann"} | key-value store |
| binary | <<"text">> | byte sequence, the efficient way to store text |
true and false are atoms, not a separate type. = is a match, not an assignment, which is why line 3 fails: Year is already bound to 1986.
Pattern matching, case and guards
Pattern matching unpacks tuples, lists and maps and selects which clause runs. Guards, written with when, add conditions to a clause. case and if are expressions that return a value.
-module(match).
-export([describe/1, grade/1, demo/0]).
describe({ok, Value}) -> io_lib:format("ok: ~p", [Value]);
describe({error, Reason}) -> io_lib:format("error: ~p", [Reason]);
describe(_Other) -> "unknown".
grade(Score) when Score >= 90 -> a;
grade(Score) when Score >= 70 -> b;
grade(_) -> c.
demo() ->
[First | Rest] = [1, 2, 3],
#{name := Name} = #{name => "Ann", age => 31},
Size = case length(Rest) of
0 -> empty;
N when N < 5 -> small;
_ -> large
end,
io:format("~p ~p ~s ~p ~p~n", [First, Rest, Name, Size, grade(75)]),
io:format("~s~n", [describe({error, timeout})]).
% match:demo() prints:
% 1 [2,3] Ann small b
% error: timeoutEvery clause except the last ends with ;, and the last one ends with .. If no clause matches, the process raises a function_clause or case_clause error.
Functions, recursion and higher-order functions
Functions are identified by name and arity, written name/Arity. There are no loops; repetition uses recursion, and tail-recursive functions run in constant stack space. Anonymous functions are written fun ... end.
-module(fns).
-export([factorial/1, sum/1, demo/0]).
factorial(0) -> 1;
factorial(N) when N > 0 -> N * factorial(N - 1).
sum(List) -> sum(List, 0). % public wrapper
sum([], Acc) -> Acc; % tail-recursive helper sum/2
sum([H | T], Acc) -> sum(T, Acc + H).
demo() ->
Double = fun(X) -> X * 2 end,
io:format("~p~n", [factorial(20)]), % 2432902008176640000
io:format("~p~n", [sum([1, 2, 3, 4])]), % 10
io:format("~p~n", [Double(21)]), % 42
io:format("~p~n", [lists:map(Double, [1, 2, 3])]), % [2,4,6]
io:format("~p~n", [[X * X || X <- [1, 2, 3, 4, 5], X rem 2 =:= 1]]). % [1,9,25]The last line uses a list comprehension. Note the comparison operators: =:= and =/= test exact equality and inequality, == and /= compare numbers by value (so 1 == 1.0 is true), and less-than-or-equal is written =<, not <=.
Lists, maps and the standard library
The lists module provides map, filter, foldl, sort, reverse and many more; maps works with maps; string and binary handle text. The full reference is in the OTP documentation.
1> L = [5, 3, 8, 1].
[5,3,8,1]
2> lists:sort(L).
[1,3,5,8]
3> lists:filter(fun(X) -> X > 2 end, L).
[5,3,8]
4> lists:foldl(fun(X, Acc) -> X + Acc end, 0, L).
17
5> M = #{ann => 31, bo => 25}, map_size(M).
2
6> M2 = M#{cy => 40}, map_size(M2).
3
7> maps:get(bo, M2).
25
8> maps:get(zed, M2, none).
noneM#{cy => 40} creates a new map; M itself never changes. Use := instead of => to update a key that must already exist.
Processes and message passing
Processes are Erlang's signature feature. spawn starts a new process, ! sends a message, and receive waits for one, matching messages by pattern. Processes are cheap, so a system can run very many of them. Links and monitors let one process notice when another dies, and OTP supervisors use this to restart failed workers automatically.
-module(counter).
-export([start/0, loop/1]).
start() ->
Pid = spawn(counter, loop, [0]),
Pid ! {add, 5},
Pid ! {add, 10},
Pid ! {get, self()},
receive
{count, N} -> io:format("count = ~p~n", [N]) % count = 15
after 1000 ->
io:format("timeout~n")
end.
loop(Count) ->
receive
{add, N} -> loop(Count + N);
{get, From} -> From ! {count, Count}, loop(Count)
end.This hand-written loop is exactly what OTP's gen_server behaviour generalizes. In production code you would write a gen_server module and place it under a supervisor.
Error handling and "let it crash"
Erlang has three classes of exceptions: error, throw and exit. Expected failures are normally returned as {ok, Value} or {error, Reason} tuples. Unexpected ones are often not caught at all: the process crashes, and its supervisor restarts it in a known good state. When you do need to catch, use try ... catch.
1> file:read_file("missing.txt").
{error,enoent}
2> try 1 / 0 catch error:badarith -> division_failed end.
division_failed
3> try throw(my_reason) catch throw:R -> {caught, R} end.
{caught,my_reason}
4> case file:read_file("missing.txt") of {ok, Bin} -> Bin; {error, Why} -> Why end.
enoentA small complete program: word frequency
This module counts words in a string with a map, sorts them by count and prints the three most common. It combines string functions, lists:foldl, maps and a custom sort.
-module(wordfreq).
-export([top/2, main/0]).
top(Text, N) ->
Words = string:lexemes(string:lowercase(Text), " .,;:!?\n"),
Counts = lists:foldl(fun(W, Acc) ->
maps:update_with(W, fun(C) -> C + 1 end, 1, Acc)
end, #{}, Words),
Sorted = lists:sort(fun({W1, C1}, {W2, C2}) ->
{-C1, W1} =< {-C2, W2}
end, maps:to_list(Counts)),
lists:sublist(Sorted, N).
main() ->
Text = "The cat and the dog. The dog sat, and the cat ran.",
[io:format("~s: ~p~n", [W, C]) || {W, C} <- top(Text, 3)],
ok.
% the: 4
% and: 2
% cat: 2Where to go next
- The Erlang/OTP documentation at erlang.org, including the Getting Started guide and the OTP Design Principles.
- Learn You Some Erlang for Great Good!, a free online book.
- rebar3, the standard build tool, and Hex, the package manager shared with Elixir.
- The
gen_serverandsupervisorreference pages once you build real services.
FAQ
Is Erlang still used?
Yes. Erlang still runs telecom systems, messaging services and RabbitMQ, and the BEAM ecosystem keeps growing through Elixir and Gleam, which can use Erlang libraries.
What is OTP in Erlang?
OTP is a set of libraries, tools and design principles shipped with Erlang. It provides behaviours such as gen_server, supervisor and application that structure fault-tolerant programs.
Should I learn Erlang or Elixir?
Elixir has a friendlier syntax and a larger web ecosystem, while Erlang is smaller and is the language OTP and many core libraries are written in. Both run on the same VM, and knowing one makes the other easy to read.
What does let it crash mean?
Instead of handling every possible error inside a process, you let the process fail and rely on a supervisor to restart it in a clean state. Error handling code stays small and the system recovers automatically.
Why are Erlang variables uppercase?
Erlang uses capitalization to tell variables from atoms: Name is a variable, while name is an atom, a constant value.