No sections found
We couldn't find anything matching your search query. Try adjusting your keywords.
Learn Modern Erlang for Python and Javascript Developers
In 2026, building distributed systems is standard. But as architectures span multiple servers and microservices, developers face a wall: race conditions, unhandled exceptions crashing entire services, and complex deployment downtimes. Enter Erlang.
Created by Ericsson to power telecom switches, Erlang powers massive concurrent systems (like WhatsApp and Discord). It provides the holy grail of reliability: 99.9999999% uptime. It achieves this not by catching every error, but through the "Let it crash" philosophy, lightweight isolated processes (the Actor model), and the ability to swap code in memory without ever restarting the system. It's time to build unbreakable systems.
2. The Rosetta Stone: Types Grid
Unlike Python and JS, Erlang is dynamically but strongly typed. It lacks static type checking at compile-time by default (though developers use Dialyzer for this), but it strictly refuses to add an integer to a string. The secret weapon is Pattern Matching and Atoms.
| Concept | Erlang (Dynamic + Strong) | Python 3.12+ | JS (ES2026) |
|---|---|---|---|
| Integer | X = 42. | x = 42 | const x = 42; |
| Atom (Symbol) | Status = ok. | N/A (Strings used) | Symbol("ok") |
| String (List of chars) | Name = "Hi". | name = "Hi" | const name = "Hi"; |
| Binary (Fast String) | Bin = <<"Hi">>. | bin = b"Hi" | Buffer.from("Hi") |
| Tuple (Fixed length) | Result = {ok, 200}. | result = ("ok", 200) | const result = ["ok", 200]; |
| List (Immutable) | Nums = [1, 2, 3]. | nums = [1, 2, 3] | const nums = [1, 2, 3]; |
| Dictionary/Map | User = #{id => 1}. | user = {"id": 1} | const user = {id: 1}; |
| Process ID (PID) | Pid = self(). | os.getpid() | process.pid |
Name, SecretNumber). Anything starting with a lowercase letter is an Atom (e.g., ok, error, apple). Atoms are memory-efficient constants used everywhere to tag data (like {ok, Value}).
3. Guess the Number Game
Erlang Features Introduced: Modules, Exporting functions, Pattern Matching in case statements, and Recursion (since Erlang has no while loops).
-module(guess).
%% Exporting functions makes them public. Format is [function_name/arity].
-export([start/0]).
start() ->
%% Generate a random number from 1 to 100
Secret = rand:uniform(100),
%% ~n is the newline character in Erlang format strings
io:format("Guess the number between 1 and 100!~n"),
%% Instead of a while loop, we call a recursive function
loop(Secret).
loop(Secret) ->
%% io:fread reads formatted input. "~d" expects an integer.
%% It returns a tagged tuple like {ok, [Number]} or {error, Reason}.
case io:fread("> ", "~d") of
%% The 'when' clause is called a Guard. It adds logic to pattern matching.
{ok, [Guess]} when Guess < Secret ->
io:format("Higher!~n"),
loop(Secret); %% Recurse
{ok, [Guess]} when Guess > Secret ->
io:format("Lower!~n"),
loop(Secret);
{ok, [_Guess]} ->
%% We matched the integer, and it wasn't > or <, so it must be ==
io:format("You win!~n");
%% The underscore acts as a wildcard catch-all
_ ->
io:format("Please type a valid number!~n"),
loop(Secret)
end.
import random
def main():
secret = random.randint(1, 100)
print("Guess the number between 1 and 100!")
while True:
try:
guess = int(input("> ").strip())
except ValueError:
print("Please type a valid number!")
continue
if guess < secret:
print("Higher!")
elif guess > secret:
print("Lower!")
else:
print("You win!")
break
if __name__ == "__main__":
main()
import * as readline from 'node:readline/promises';
import { stdin as input, stdout as output } from 'node:process';
async function main() {
const rl = readline.createInterface({ input, output });
const secret = Math.floor(Math.random() * 100) + 1;
console.log("Guess the number between 1 and 100!");
while (true) {
const guess = parseInt((await rl.question('> ')).trim(), 10);
if (isNaN(guess)) {
console.log("Please type a valid number!");
continue;
}
if (guess < secret) console.log("Higher!");
else if (guess > secret) console.log("Lower!");
else {
console.log("You win!");
break;
}
}
rl.close();
}
main();
4. Arithmetic Command Line Game
Erlang Features Introduced: Function clauses (overloading by pattern matching), string parsing, and multiple return states.
-module(arithmetic).
-export([start/0]).
start() ->
io:format("Solve the addition problems! Type 'quit' to exit.~n"),
play_loop().
play_loop() ->
A = rand:uniform(10),
B = rand:uniform(10),
%% ~p prints Erlang terms. We prompt the user using io:get_line.
Input = io:get_line(io_lib:format("What is ~p + ~p? ", [A, B])),
%% We strip the newline character and handle the routing
CleanInput = string:trim(Input),
handle_input(CleanInput, A + B).
%% Erlang allows you to define multiple "clauses" for the same function.
%% The compiler checks them top-to-bottom to see which pattern matches.
%% Clause 1: The user typed "quit"
handle_input("quit", _CorrectAnswer) ->
io:format("Thanks for playing!~n");
%% Clause 2: Anything else, we try to convert it to an integer
handle_input(StringInput, CorrectAnswer) ->
try list_to_integer(StringInput) of
CorrectAnswer ->
io:format("Correct!~n"),
play_loop(); %% Loop back
_WrongAnswer ->
io:format("Wrong! It was ~p.~n", [CorrectAnswer]),
play_loop()
catch
error:badarg ->
io:format("Please enter a number or 'quit'.~n"),
play_loop()
end.
5. The Actor Model: Keeping State Without Variables
Erlang Features Introduced: Lightweight processes, Message Passing via ! (send) and receive, and keeping state in an immutable loop.
In Python and JS, if you want a counter, you mutate a variable: count += 1. In Erlang, variables are immutable (they cannot be changed once set). Instead, we spawn a tiny process (an Actor) that sits in a recursive loop, listening for messages. When it receives an "add" message, it calls itself with the new value.
-module(counter).
-export([start/0, loop/1]).
start() ->
%% spawn/3 creates a new process and returns its Process ID (PID).
%% This process takes microseconds to create and uses almost no memory.
Pid = spawn(?MODULE, loop, [0]),
io:format("Counter started with PID: ~p~n", [Pid]),
Pid.
%% This function runs infinitely in its own process
loop(Count) ->
%% 'receive' blocks the process until a message arrives in its mailbox
receive
{add, Amount} ->
NewCount = Count + Amount,
io:format("Counter is now: ~p~n", [NewCount]),
%% Recurse with the new state
loop(NewCount);
{get, CallerPid} ->
%% Send (!) the current count back to the caller
CallerPid ! {current_count, Count},
%% State hasn't changed, recurse with the same Count
loop(Count);
stop ->
io:format("Stopping counter.~n"),
ok %% Process naturally dies when the function ends
end.
%% Usage in REPL:
%% Pid = counter:start().
%% Pid ! {add, 5}.
%% Pid ! stop.
6. The Famous Erlang Ring Benchmark
One of the classic demonstrations of Erlang's power is the Ring benchmark. It involves spawning N processes in a ring topology (Process 1 sends to Process 2, which sends to Process 3 ... which sends to Process 1). Then, a message is sent M times around the entire ring.
In Python or Node.js, spawning 100,000 OS threads or trying to chain 100,000 callbacks will instantly crash your machine with Out of Memory errors. Erlang can spawn millions of processes on a standard laptop in seconds.
-module(ring).
-export([start/2, process_node/1]).
%% N = Number of processes in the ring
%% M = Number of times to send a message around the ring
start(N, M) ->
%% Start the creation process from the end of the ring to the beginning
%% We pass self() so the last node knows who started the chain
FirstPid = create_ring(N, self()),
%% Send the initial message into the ring
FirstPid ! {message, M},
%% Wait for the message to traverse M times and come back to the main process
receive
done ->
io:format("Finished passing message ~p times around ~p nodes!~n", [M, N])
end.
%% create_ring/2 builds the ring backwards
create_ring(1, NextPid) ->
%% The very first node just points to the next PID
spawn(?MODULE, process_node, [NextPid]);
create_ring(N, NextPid) ->
%% Spawn a node that points to the next PID, then recurse
CurrentPid = spawn(?MODULE, process_node, [NextPid]),
create_ring(N - 1, CurrentPid).
%% Every node sits in this loop, waiting for a message
process_node(NextPid) ->
receive
{message, 0} ->
%% We've passed the message M times. Stop propagating and send 'done' to next.
NextPid ! done;
{message, TripsLeft} ->
%% Forward the message, decrementing the counter
NextPid ! {message, TripsLeft - 1},
%% Stay alive for potential further messages
process_node(NextPid);
done ->
%% The completion signal is propagating, pass it on and die.
NextPid ! done
end.
7. Erlang's Superpower: Hot Code Swapping
Imagine a telecom switch handling 100,000 phone calls. You find a bug in the routing logic. In Python or Node.js, you have to spin up a new instance, migrate traffic, and shut down the old one. In Erlang, you simply compile the new code, and the system updates itself while it is running, without dropping a single call.
The "Two Versions" Rule
Erlang memory management allows exactly two versions of a module to exist at any given time: Old and Current. When you compile a new file, the Current becomes Old, and the new file becomes Current.
How to Trigger It
If a process calls a function locally (e.g. loop(State)), it stays in the version of code it was running. To jump to the newest code, it must make a Fully Qualified Call using the module name: ?MODULE:loop(State).
-module(server).
-export([loop/1]).
loop(State) ->
receive
upgrade ->
%% This jumps to the newest compiled code!
server:loop(State);
Msg ->
handle(Msg),
%% This stays in the exact same memory space
loop(State)
end.
Warning: If a third version is compiled, any processes still lingering in the "Old" version are instantly killed by the VM to prevent memory leaks.
Hot Reloading Lifecycle
1,000 processes running the loop.
Old processes finish tasks in V1. When they hit `?MODULE:loop()`, they jump instantly to V2.
True zero-downtime deployments.
8. The REPL (erl) & Tooling Basics
The Erlang shell (erl) is arguably the most powerful REPL of any programming language. You can use it to connect to remote servers halfway across the world, inspect running processes, and compile code on the fly.
c(module_name).
Compiles the file module_name.erl and immediately loads it into the current running memory. This is how you trigger hot code swapping locally.
f(). and f(Var).
Because variables in Erlang are strictly immutable, you cannot do X = 1. and then later X = 2. in the shell. Running f() "forgets" all bindings in the REPL, allowing you to reuse variable names.
q(). or Ctrl+G -> q -> Enter
How to exit the REPL. q() is a shortcut for init:stop(), which gracefully shuts down the Erlang VM and all its processes.
9. Tips, Tricks & Beginner Gotchas
Erlang's syntax is influenced by Prolog. Here are the immediate "gotchas" you should know.
Gotcha: Punctuation Matters
Forget braces and indentation limits. Erlang controls flow using punctuation.
Comma (,): Means "and then". Separates sequential statements.
Semicolon (;): Means "or". Separates clauses in an if, case, or function body.
Period (.): Means "end". Concludes a function entirely.
= is NOT Assignment
In Erlang, = is the Pattern Match Operator.
X = 1 means "Make X equal 1 so this equation is true."
If you write 1 = X later, the program succeeds because 1 does equal 1. If you write 2 = X, it crashes with a badmatch error because 2 cannot equal 1. It does NOT reassign X to 2.
Strings are Lists (Usually)
Double quoted strings "Hello" are actually linked lists of integers (ASCII values). This is very slow for large data. Modern Erlang code heavily uses Binaries for text: <<"Hello">>. These are contiguous memory allocations, drastically faster for network parsing and JSON.
10. OTP Framework & System Design
Nobody writes bare spawn/3 and receive loops in production. You use OTP (Open Telecom Platform), which is included in Erlang. It is a set of design patterns and behaviors that handles all the edge cases of distributed systems.
Instead of managing your own mailboxes, gen_server handles message timeouts, synchronous calls (where you wait for an answer), asynchronous casts (fire and forget), and system debugging out of the box.
-module(my_server).
-behavior(gen_server).
%% API
-export([start_link/0, get_count/0, increment/0]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2]).
%% API Functions (Hidden from the user)
start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, 0, []).
get_count() -> gen_server:call(?MODULE, get_count).
increment() -> gen_server:cast(?MODULE, increment).
%% Callbacks
init(InitialCount) -> {ok, InitialCount}.
%% Synchronous (Call)
handle_call(get_count, _From, Count) ->
%% Reply back with Count, and keep Count as the state
{reply, Count, Count}.
%% Asynchronous (Cast)
handle_cast(increment, Count) ->
%% Update state, no reply needed
{noreply, Count + 1}.
You don't write try/catch blocks in Erlang. Instead, you link workers to Supervisors. If a database connection fails, the worker process crashes. The Supervisor detects this, logs the error, and instantly starts a fresh, clean worker in its place. This avoids systems getting trapped in weird, corrupt states.
Rebar3 is the build tool for Erlang (like npm or pip). Hex.pm is the package manager. To create a new application, you type rebar3 new app my_project, and to compile, you type rebar3 compile. Don't build production software without it.