Elixir & Erlang/OTP Module 3 – Pattern matching and control flow
Pattern matching, the match operator and the pin
How Elixir's = matches patterns: take tuples, lists, maps and strings apart, rebind or pin a name with ^, read a MatchError and decide when to crash.
What you will learn
- Match tuples, lists, maps and strings with = to take their values apart
- Use the pin operator ^ to match the value a name already has instead of binding it again
- Predict which values a pattern rejects, from sizes, literals, repeated names and missing keys
- Read a MatchError and decide whether a failed match should stop the program
Before you start
What you’ll learn · Before you start
What you will learn
- Match tuples, lists, maps and strings with = to take their values apart
- Use the pin operator ^ to match the value a name already has instead of binding it again
- Predict which values a pattern rejects, from sizes, literals, repeated names and missing keys
- Read a MatchError and decide whether a failed match should stop the program
Before you start
On this page
{port, ""} = Integer.parse("4000") turns the text “4000” into the number 4000 and, in the same line, checks that
nothing came after the digits. Given “40x”, Integer.parse/1 returns {40, "x"}, the shape {port, ""} does not fit,
and the line stops with a MatchError: The exception that = raises when the value on its right does not fit the pattern on its left. Its message, no match of right hand side value, is followed by the value that did not fit. instead of carrying on with 40. That = is not
the assignment of other languages: it is Elixir’s match operator: =, which matches the value on its right against the pattern on its left. When they fit, it binds the names of the pattern and returns the value; when they do not, it raises MatchError., which compares a
value with a pattern: The left side of a match, such as {:ok, port} or [head | tail]: the shape a value must have. Literals in it must equal the parts of the value, and the names in it are bound to whatever stands in their place. and binds the names in it.
Below, you will take tuples, lists, maps and strings apart with patterns, see why x = 2 after x = 1 works while
^x = 3 raises an error, predict which values a pattern rejects, and decide when a failed match should stop a
program. The sessions below ran on Elixir 1.20.4 with Erlang/OTP 29.1.1, and each shows what it printed.
A match checks a value’s shape and binds its names
Type x = 1 into IEx and the name x is bound to 1, as in any language. Now type 1 = x: it works too, and returns
1, because = only asks whether the two sides fit together. 2 = x does not fit, and raises a MatchError. The
Elixir guide says it plainly: the = operator “is actually called the match operator”.
The left side is the pattern and the right side is a value, worked out first. A name in the pattern matches any value
and is bound to it. A literal, such as :ok, 1 or "", matches only a value equal to it. A tuple pattern matches
only a tuple of the same size whose elements match one by one. Here are matches that fit and matches that do not:
from session import shell
# The match operator: the value on the right, a pattern on the left. Names bind; literals must be equal.
shell([
"x = 1",
"1 = x",
"2 = x",
'{status, body} = {:ok, "data"}',
"body",
"{a, b} = {1, 2, 3}",
"{:ok, value} = {:error, :enoent}",
"{:ok, 1} = {:ok, 1.0}",
'{port, ""} = Integer.parse("4000")',
'{port, ""} = Integer.parse("40x")',
"port",
"{_, second, _} = {:a, :b, :c}",
"second",
"System.halt()",
]) Output
$ iex
Erlang/OTP 29 [erts-17.1] [source] [64-bit] [smp:18:18] [ds:18:18:10] [async-threads:1] [jit]
Interactive Elixir (1.20.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> x = 1
1
iex(2)> 1 = x
1
iex(3)> 2 = x
** (MatchError) no match of right hand side value:
1
(stdlib 8.1) erl_eval.erl:796: :erl_eval.expr/6
iex:3: (file)
iex(3)> {status, body} = {:ok, "data"}
{:ok, "data"}
iex(4)> body
"data"
iex(5)> {a, b} = {1, 2, 3}
** (MatchError) no match of right hand side value:
{1, 2, 3}
(stdlib 8.1) erl_eval.erl:796: :erl_eval.expr/6
iex:5: (file)
iex(5)> {:ok, value} = {:error, :enoent}
** (MatchError) no match of right hand side value:
{:error, :enoent}
(stdlib 8.1) erl_eval.erl:796: :erl_eval.expr/6
iex:5: (file)
iex(5)> {:ok, 1} = {:ok, 1.0}
** (MatchError) no match of right hand side value:
{:ok, 1.0}
(stdlib 8.1) erl_eval.erl:796: :erl_eval.expr/6
iex:5: (file)
iex(5)> {port, ""} = Integer.parse("4000")
{4000, ""}
iex(6)> {port, ""} = Integer.parse("40x")
** (MatchError) no match of right hand side value:
{40, "x"}
(stdlib 8.1) erl_eval.erl:796: :erl_eval.expr/6
iex:6: (file)
iex(6)> port
4000
iex(7)> {_, second, _} = {:a, :b, :c}
{:a, :b, :c}
iex(8)> second
:b
iex(9)> System.halt()
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 match_session.py
{status, body} = {:ok, "data"} bound two names at once. {a, b} failed on a tuple of three elements, and
{:ok, value} failed on {:error, :enoent}, because its literal :ok met :error. {:ok, 1} failed on
{:ok, 1.0}: the patterns guide says that numbers in patterns are compared strictly, so 1 matches 1 and not 1.0,
although 1 == 1.0 is true. And a match binds all of its names or none: after the failed match on “40x”, port
still named 4000.
The last two lines used _, which matches anything and binds nothing, to take the middle of three elements. A name
that starts with an underscore, such as _status, also matches anything and says what it ignores. Both are
underscore variable: _ or a name that starts with _, such as _status. It matches any value and says that the value is not used; _ itself can never be read, and the compiler warns when a name like _status is.: _ can never be read, and reading a name such as
_status draws a warning, as a later section shows. Step through the opening’s pattern, matched first against
Integer.parse("4000") and then against Integer.parse("40x"):
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- The right side runs first. Integer.parse("4000") runs before anything is matched. It returns a tuple of the number it read and the text left after it: {4000, ""}, with nothing left over.
- The same shape?. The pattern is a tuple of two elements, and so is the value, so the match goes on, one position at a time. A tuple of three elements, or a list, would fail here at once.
- A name binds. Position 0 of the pattern is the name port. A name matches any value and is bound to it, so port will name 4000.
- A literal must be equal. Position 1 of the pattern is the literal "", the empty string. A literal matches only a value equal to it: nothing may follow the digits, and nothing does.
- The match succeeds. Every position fits, so port is bound to 4000, and the whole expression returns the value on the right, {4000, ""}, which is what IEx printed.
- Now with "40x". Integer.parse("40x") reads 40 and leaves "x", so the value is {40, "x"}. The shape fits, and position 0 would bind port to 40.
- The literal does not fit. "x" is not equal to "", so the match fails as a whole. No name is bound, port still names 4000, and = raises MatchError with the value that did not fit.
The value was ready before any comparison began, the shapes were compared first, and then each position: the name took whatever was there, and the literal decided whether the whole match succeeded.
Key idea
= works out the value on its right, then fits the pattern on its left to it: the same shape and size, every
literal equal. The names are bound only when everything fits; otherwise the match raises MatchError.
Checkpoint
Check: Matching a value against a pattern
Lists match by head and tail, maps by the keys you name
A list pattern comes in two forms. [a, b] lists every item, so it matches only a list of exactly two items.
[first | rest] names the head and tail: The first item of a list and the list of every item after it: in [1, 2, 3] the head is 1 and the tail is [2, 3]. [x | list] builds a list from a head and a tail. More in “Lists, tuples and ranges”: the first item, and the list of every item
after it, which may be empty. Map patterns work differently again, as the second half of this session shows:
from session import shell
# Lists match by head and tail, or item by item; maps match the keys a pattern names.
shell([
"[first | rest] = [3, 1, 4, 1, 5]",
"rest",
"[a, b | _] = [3, 1, 4, 1, 5]",
"b",
"[a, b] = [3, 1, 4, 1, 5]",
"[head | _] = []",
'profile = %{name: "Mira", age: 30}',
"%{name: name} = profile",
"name",
"%{} = profile",
"%{email: email} = profile",
'%{user: %{city: city}} = %{id: 7, user: %{name: "Mira", city: "Pune"}}',
"city",
"System.halt()",
]) Output
$ iex
Erlang/OTP 29 [erts-17.1] [source] [64-bit] [smp:18:18] [ds:18:18:10] [async-threads:1] [jit]
Interactive Elixir (1.20.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> [first | rest] = [3, 1, 4, 1, 5]
[3, 1, 4, 1, 5]
iex(2)> rest
[1, 4, 1, 5]
iex(3)> [a, b | _] = [3, 1, 4, 1, 5]
[3, 1, 4, 1, 5]
iex(4)> b
1
iex(5)> [a, b] = [3, 1, 4, 1, 5]
** (MatchError) no match of right hand side value:
[3, 1, 4, 1, 5]
(stdlib 8.1) erl_eval.erl:796: :erl_eval.expr/6
iex:5: (file)
iex(5)> [head | _] = []
** (MatchError) no match of right hand side value:
[]
(stdlib 8.1) erl_eval.erl:796: :erl_eval.expr/6
iex:5: (file)
iex(5)> profile = %{name: "Mira", age: 30}
%{name: "Mira", age: 30}
iex(6)> %{name: name} = profile
%{name: "Mira", age: 30}
iex(7)> name
"Mira"
iex(8)> %{} = profile
%{name: "Mira", age: 30}
iex(9)> %{email: email} = profile
** (MatchError) no match of right hand side value:
%{name: "Mira", age: 30}
(stdlib 8.1) erl_eval.erl:796: :erl_eval.expr/6
iex:9: (file)
iex(9)> %{user: %{city: city}} = %{id: 7, user: %{name: "Mira", city: "Pune"}}
%{id: 7, user: %{name: "Mira", city: "Pune"}}
iex(10)> city
"Pune"
iex(11)> System.halt()
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 collections_session.py
[first | rest] took 3 and left [1, 4, 1, 5] in rest, and [a, b | _] took the first two items whatever came
after them. [a, b] failed on the list of five, and [head | _] failed on [], because an empty list has no head.
The map pattern %{name: name} matched a map that also holds :age: the patterns guide calls this a subset match,
so a map pattern needs only the keys it names. %{} matched as well, since the empty map
pattern matches every map, while {} and [] match only the empty tuple and the empty list. %{email: email}
failed, because that key is missing, and a pattern nests as deep as the data: %{user: %{city: city}} read the city
two levels down. The table puts the rules side by side:
| Pattern | Matches | Fails on |
|---|---|---|
{a, b} |
a tuple of exactly 2 elements | {1, 2, 3} |
[a, b] |
a list of exactly 2 items | [3, 1, 4, 1, 5] |
[first | rest] |
a list of 1 item or more | [] |
[a, b | _] |
a list of 2 items or more | [7] |
%{name: n} |
any map with a :name key |
%{age: 30} |
%{} |
any map at all | [name: "Mira"] |
The last row is a reminder from the lesson on keyword lists: [name: "Mira"] is a list of tuples, so no map pattern
matches it. Keys in a map pattern must be literals, or variables marked with ^, which the section after next
explains.
Taking the first two items with [a, b]
[a, b] = list fails for every list that does not hold exactly two items, as it failed on the list of five. To
take the first two items of a longer list, write [a, b | _] = list, which matches any list of two items or more.
Checkpoint
Check: List and map patterns
A string pattern can take a fixed prefix off the front
Elixir strings are binary: A sequence of bytes, written <<104, 105>>. An Elixir string is a binary whose bytes are valid UTF-8, so "hi" and <<104, 105>> are the same value. More in “Strings, binaries and charlists”, sequences of bytes, and a pattern can match their beginning:
"deposit:" <> amount matches any string that starts with the 8 bytes of “deposit:” and binds amount to the rest.
This script reads commands that way, then takes one byte off the front of another string with <<first, rest::binary>>:
# Commands arrive as text, such as "deposit:250". A pattern with <> takes a fixed prefix off the front.
"deposit:" <> amount = "deposit:250"
IO.inspect(amount, label: "after the prefix")
# amount is still text. Integer.parse/1 reads the number, and "" checks that nothing follows it.
{number, ""} = Integer.parse(amount)
IO.inspect(number, label: "as a number")
# The same kind of pattern works for any fixed prefix.
"user:" <> id = "user:42"
IO.inspect(id, label: "the id")
# <<first, rest::binary>> takes one byte off the front; rest is every byte after it.
<<first, rest::binary>> = "ok"
IO.inspect({first, rest}, label: "first byte and the rest")
# A string that starts with another prefix does not match.
"deposit:" <> _ = "withdraw:40"
IO.puts("this line never runs") from session import run
# The script stops at its last match, so the run is expected to fail.
run("""
!elixir commands.exs
""") Output
$ elixir commands.exs
after the prefix: "250"
as a number: 250
the id: "42"
first byte and the rest: {111, "k"}
** (MatchError) no match of right hand side value:
"withdraw:40"
commands.exs:18: (file)
(elixir 1.20.4) lib/code.ex:1639: Code.require_file/2
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 commands_session.py
amount came back as “250”, a string, so the script read the number with Integer.parse/1, and "" checked that
nothing followed it. "user:" <> id took the id “42” off its prefix. <<first, rest::binary>> bound first to 111,
the byte of the letter o, and rest to “k”. The last match failed, because “withdraw:40” does not start with
“deposit:”, and the script stopped on line 18 with the whole string in the error. Step through the first match, byte
by byte:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- A string is bytes. A string is a binary: "deposit:250" is 11 bytes, one for each character, because every character here is ASCII. The pattern describes it as a fixed prefix followed by the rest.
- The prefix is compared. The prefix "deposit:" is a literal of 8 bytes, so the match compares the first 8 bytes of the string with it, one by one. They are equal.
- The rest is bound. Everything after the prefix is bound to amount: the 3 bytes "250". amount is still a string, so the script reads the number with Integer.parse/1.
- Another prefix. "withdraw:40" starts with other bytes, so the same pattern does not fit, and = raises MatchError with the whole string, where the script stopped.
- A suffix is refused. In id <> ":user" the part of unknown size comes first, so no match could tell where id ends. Elixir refuses the pattern with ArgumentError while it reads the code, before anything runs.
A prefix works because its size is known: the match compares that many bytes, and whatever is left is the rest. The
last step shows the other way round. In id <> ":user" nothing says where id ends, so Elixir refuses the pattern
while it reads the code, with an ArgumentError that says the left operand of <> “has unknown size”, as the last
section of this lesson shows. To look at the end of a string, use a function such as String.ends_with?/2, or split
the string, as the practice below does.
Checkpoint
Check: String prefixes with <>
A name can be bound again; the pin compares instead
A name on the left of = is always bound, even when it already has a value. The Elixir guide says so directly:
“Variables in Elixir can be rebound”. When a pattern must compare with the value a name already has, put the
pin operator: ^ before a name in a pattern, as in ^x = 3. The pinned name stands for the value it already has and is compared like a literal instead of being bound again. in front of the name:
from session import shell
# Rebinding a name, the pin operator, a name used twice in one pattern, and a pinned map key.
shell([
"x = 1",
"x = 2",
"^x = 3",
"^x = 2",
"{y, ^x} = {5, 2}",
"{y, ^x} = {5, 1}",
"{a, a} = {1, 1}",
"{a, a} = {1, 2}",
"key = :city",
'%{^key => place} = %{city: "Pune", pin: "411001"}',
"place",
"System.halt()",
]) Output
$ iex
Erlang/OTP 29 [erts-17.1] [source] [64-bit] [smp:18:18] [ds:18:18:10] [async-threads:1] [jit]
Interactive Elixir (1.20.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> x = 1
1
iex(2)> x = 2
2
iex(3)> ^x = 3
** (MatchError) no match of right hand side value:
3
(stdlib 8.1) erl_eval.erl:796: :erl_eval.expr/6
iex:3: (file)
iex(3)> ^x = 2
2
iex(4)> {y, ^x} = {5, 2}
{5, 2}
iex(5)> {y, ^x} = {5, 1}
** (MatchError) no match of right hand side value:
{5, 1}
(stdlib 8.1) erl_eval.erl:796: :erl_eval.expr/6
iex:5: (file)
iex(5)> {a, a} = {1, 1}
{1, 1}
iex(6)> {a, a} = {1, 2}
** (MatchError) no match of right hand side value:
{1, 2}
(stdlib 8.1) erl_eval.erl:796: :erl_eval.expr/6
iex:6: (file)
iex(6)> key = :city
:city
iex(7)> %{^key => place} = %{city: "Pune", pin: "411001"}
%{pin: "411001", city: "Pune"}
iex(8)> place
"Pune"
iex(9)> System.halt()
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 pin_session.py
x = 2 bound x again. ^x = 3 raised MatchError, because ^x stood for 2, and ^x = 2 matched. In
{y, ^x} = {5, 2} the pin compared position 1 while y was bound, and {y, ^x} = {5, 1} failed. A name used twice
in one pattern must match the same value both times: {a, a} = {1, 1} matched and {a, a} = {1, 2} did not. The
pin is also how a variable becomes a key in a map pattern: %{^key => place} read the :city key, the value of
key. Here are four of those lines as names and values:
The drawing shows the whole scene; its 4 steps are listed below.
Every step as text
- x = 1. x = 1 matches the value 1 against the pattern x. A name matches anything, so x is bound to 1.
- x = 2. x = 2 is a new match with the name x on the left, so x is bound again, now to 2. The value 1 does not change: x simply names another value.
- ^x = 3. With the pin, ^x is not a name to bind. It stands for the value x has now, 2, and is compared like a literal: 3 is not 2, so the match raises MatchError and x stays 2.
- {y, ^x} = {5, 2}. Inside a larger pattern the pin still compares: y is bound to 5, and ^x checks that position 1 holds 2. It does, so the match succeeds.
The name x moved from 1 to 2, the value 1 stayed as it was, and the pinned ^x was compared each time and never
bound. Forgetting the pin is the classic slip of this section:
Forgetting the pin when you mean a known value
With id = 7 set earlier, {:reply, id, text} = reply does not check the reply’s id: it binds id again, to
whatever the reply holds, and accepts the reply to any request. Write {:reply, ^id, text} = reply to accept only
the reply to request 7.
rebinding: Binding a name that already has a value to another value, as x = 2 after x = 1. Elixir allows it, and the old value does not change; Erlang does not, because there a variable is bound only once. changes which value a name refers to, never the value itself. Erlang, which runs
on the same VM, has no rebinding at all: its reference manual says that “a variable can only be bound once”, so in
Erlang every bound variable in a pattern compares, as ^x does in Elixir:
from session import shell
# The same matches in the Erlang shell, where a variable is bound only once.
shell([
"X = 1.",
"X = 1.",
"X = 2.",
"{ok, Port} = {ok, 4000}.",
"Port.",
"halt().",
], command="erl", kind="erl") Output
$ erl
Erlang/OTP 29 [erts-17.1] [source] [64-bit] [smp:18:18] [ds:18:18:10] [async-threads:1] [jit]
Eshell V17.1 (press Ctrl+G to abort, type help(). for help)
1> X = 1.
1
2> X = 1.
1
3> X = 2.
** exception error: no match of right hand side value 2
4> {ok, Port} = {ok, 4000}.
{ok,4000}
5> Port.
4000
6> halt().
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 erlang_session.py
X = 1. worked a second time because X was already 1, and X = 2. failed with “no match of right hand side
value 2”. Rebinding makes one slip easy: a value bound and never read before its name is bound again. The compiler
points it out, and it also complains when a name that starts with an underscore is read after all:
# The first value of total is never read before total is bound again.
total = 0
total = 10
IO.puts(total)
# A leading underscore says a value is ignored on purpose. Reading _code anyway draws a warning.
{_status, body} = {:ok, "data"}
IO.puts(body)
{_code, text} = {404, "not found"}
IO.puts("#{_code}: #{text}") from session import run
run("""
elixir rebind.exs
""") Output
$ elixir rebind.exs
warning: the underscored variable "_code" is used after being set. A leading underscore indicates that the value of the variable should be ignored. If this is intended please rename the variable to remove the underscore
│
10 │ IO.puts("#{_code}: #{text}")
│ ~
│
└─ rebind.exs:10:12
warning: variable "total" is unused (if the variable is not meant to be used, prefix it with an underscore)
│
2 │ total = 0
│ ~~~~~
│
└─ rebind.exs:2:1
10
data
404: not found
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 rebind_session.py
The first total was never read, so the compiler called it unused, and _code was read although its underscore said
it would not be. Both warnings came before the script printed anything, because the compiler reads the whole file
first.
Key idea
A name on the left of = is bound, whatever it held before; ^name uses the value it holds and compares it like a
literal.
Checkpoint
Check: Rebinding and the pin operator
Why a match fails, and when that should stop the program
Every failure of this lesson came from one of five checks: the shape or the size ({a, b} against three elements),
a literal (:ok against :error, 1 against 1.0), a name used twice ({a, a} against {1, 2}), a list that is too
short ([head | _] against []), and a missing key (%{email: email}). Each raised MatchError while the program
ran, with the value that did not fit. A few lines never run at all, because Elixir refuses them while it reads the
code: a function call on the left of =, a pin on a name that has no value, a part of unknown size before <>, and
_ read as a value:
from session import shell
# Patterns that never run: Elixir refuses them while it reads the code, before anything is matched.
shell([
"length([1, 2]) = 2",
"^z = 1",
'id <> ":user" = "42:user"',
"_",
"System.halt()",
]) Output
$ iex
Erlang/OTP 29 [erts-17.1] [source] [64-bit] [smp:18:18] [ds:18:18:10] [async-threads:1] [jit]
Interactive Elixir (1.20.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> length([1, 2]) = 2
error: cannot invoke remote function :erlang.length/1 inside a match
└─ iex:1
** (CompileError) cannot compile code (errors have been logged)
iex(1)> ^z = 1
error: undefined variable ^z. No variable "z" has been defined before the current pattern
└─ iex:1
** (CompileError) cannot compile code (errors have been logged)
iex(1)> id <> ":user" = "42:user"
** (ArgumentError) cannot perform prefix match because the left operand of <> has unknown size. The left operand of <> inside a match should either be a literal binary or an existing variable with the pin operator (such as ^some_var). Got: id
(elixir 1.20.4) lib/kernel.ex:2210: Kernel.invalid_concat_left_argument_error/1
(elixir 1.20.4) lib/kernel.ex:2185: Kernel.wrap_concatenation/3
(elixir 1.20.4) lib/kernel.ex:2164: Kernel.extract_concatenations/2
(elixir 1.20.4) expanding macro: Kernel.<>/2
iex:1: (file)
iex(1)> _
error: invalid use of _. _ can only be used inside patterns to ignore values and cannot be used in expressions. Make sure you are inside a pattern or change it accordingly
└─ iex:1
** (CompileError) cannot compile code (errors have been logged)
iex(1)> System.halt()
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 refused_session.py
Each line was refused before anything was matched: three with a CompileError and the <> pattern with an
ArgumentError, raised while the macro <> was expanded. A failed match, by contrast, is an assertion that failed
while the program ran.
Whether that should stop the program depends on what the failure means. A service reads its settings when it starts
and cannot run without them, so {:ok, text} = File.read(path) states that assumption. Here it is, with the same read
written with File.read!/1, both run while the file is missing and once it exists:
# A service reads its settings when it starts. It cannot run without them, so the match states that assumption.
{:ok, text} = File.read("settings.json")
IO.puts("read #{byte_size(text)} bytes of settings") # The same read with File.read!/1, which raises an error that says what went wrong in words.
text = File.read!("settings.json")
IO.puts("read #{byte_size(text)} bytes of settings") from session import run
# Both scripts stop while settings.json is missing; once it exists, they run.
run("""
!elixir startup.exs
!elixir startup_bang.exs
echo '{"port": 4000}' > settings.json
elixir startup.exs
""") Output
$ elixir startup.exs
** (MatchError) no match of right hand side value:
{:error, :enoent}
startup.exs:2: (file)
(elixir 1.20.4) lib/code.ex:1639: Code.require_file/2
[exit status 1]
$ elixir startup_bang.exs
** (File.Error) could not read file "settings.json": no such file or directory
(elixir 1.20.4) lib/file.ex:451: File.read!/2
startup_bang.exs:2: (file)
[exit status 1]
$ echo '{"port": 4000}' > settings.json
$ elixir startup.exs
read 15 bytes of settings
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 startup_session.py
Both scripts stopped with exit status 1. The match reported only the value, {:error, :enoent}, which the File
documentation explains as “the file does not exist”; File.read!/1 said the same in words, with the file’s name.
This is what Erlang and Elixir call let it crash: The Erlang and Elixir habit of letting code fail at once when something unexpected happens, instead of rescuing every possible error, so that a supervisor can start the work again in a known state.: when something unexpected happens,
the Elixir guide advises letting the exception happen rather than rescuing it, so that a supervisor can start the
work again from a known state, as the lessons on OTP will show.
The same guide draws the line at the word unexpected. A file name a user typed may well be wrong, and a script should
answer that with a clear message, not a crash. That is the work of case, the next lesson: it tries several patterns
in turn and runs the code of the first that fits. And match?/2 only asks whether a value fits: it answers true or
false, and no name of its pattern is bound after it. Two questions decide which to write:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| Write | When the read fails | What the error says | The contents |
|---|---|---|---|
| {:ok, text} = File.read(path) When it does not fit: raises MatchError | Mixed stops the program, as startup.exs stopped | Mixed only the value: {:error, :enoent} | Good bound to text when the read works |
| File.read!(path) When it does not fit: raises File.Error | Mixed stops the program, as startup_bang.exs stopped | Good the file and the reason in words: no such file or directory | Good returned when the read works |
| case File.read(path) do … When it does not fit: runs the clause you wrote | Good runs your clause for {:error, reason}, and the program goes on | Good whatever your clause says or does | Good bound inside the {:ok, text} clause |
| match?({:ok, _}, result) When it does not fit: returns false | Good returns false, and nothing stops | Poor none: only true or false | Poor not bound: names inside match? stay inside it |
How the answers decide
- “Can the failure happen in normal use, such as a file name a user typed or a network that is down?” Yes, users or the outside world can cause it: rules out {:ok, text} = File.read(path) and File.read!(path). Both stop the program at the first failure. A failure that users or the outside world can cause needs code of its own: a clause that says what went wrong.
- “Can the failure happen in normal use, such as a file name a user typed or a network that is down?” No, only a bug or a broken setup: adds a caveat to case File.read(path) do … and match?({:ok, _}, result). Code that carries on after a failure that means a bug or a broken setup hides it. Let the program stop, with the clearest error you can get.
- “Do you need the value itself, or only to know whether it fits?” I need the value: rules out match?({:ok, _}, result). match?/2 binds no name that you can use after it, so you would have to match a second time.
- “Do you need the value itself, or only to know whether it fits?” Only whether it fits: adds a caveat to {:ok, text} = File.read(path) and File.read!(path). They raise when the value does not fit. To ask only whether it fits, match?/2 answers true or false.
In short: a match, or File.read!/1 for the clearer message, when a failure means a bug or a broken setup; case
when users or the outside world can cause the failure; and match?/2 when you need only a yes or a no.
The compiler also knows what many functions return, and it warns about a pattern that can never fit. This module
guesses wrongly that Integer.parse/1 returns {:ok, _}:
defmodule PortConfig do
# A wrong guess at a return value: Integer.parse/1 never returns {:ok, _}.
def parse(text) do
{:ok, port} = Integer.parse(text)
port
end
end from session import run
run("""
cd typecheck
elixirc port_config.ex
""") Output
$ cd typecheck
$ elixirc port_config.ex
warning: the following pattern will never match:
{:ok, port} = Integer.parse(text)
because the right-hand side has type:
dynamic(:error or {integer(), binary()})
where "text" was given the type:
# type: dynamic()
# from: port_config.ex:3:13
text
type warning found at:
│
4 │ {:ok, port} = Integer.parse(text)
│ ~
│
└─ port_config.ex:4:17: PortConfig.parse/1
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 typecheck_session.py
The warning gives the type of the right side, dynamic(:error or {integer(), binary()}): :error, or a number with
the text after it. So the wrong guess is found before the code ever runs, instead of as a MatchError in production.
Checkpoint
Check: MatchError, and when to let it crash
Practise: read back a replay file
A game writes each move it plays to its own replay file, one line per move, such as “move 3 4”. Write
Replay.move/1, which reads such a line back as {:move, 3, 4}, and Replay.reply_text/2, which returns the text of
a reply {:reply, id, text} only when it answers the request id. Use matches only: <> for the prefix,
String.split/2 and a list pattern for the two numbers, Integer.parse/1 with "" for each number, and the pin for
the id. The game wrote the file itself, so a line of any other shape means the file is damaged, and the match should
fail. Copy the two files into an empty folder:
defmodule Replay do
@moduledoc "Reads back the lines a game wrote to its own replay file."
@doc """
Reads a line such as "move 3 4" as {:move, 3, 4}. The game wrote the file itself, so a line
of any other shape means the file is damaged: let the match fail with a MatchError.
"""
def move(_line) do
# Drop the underscore once you use the argument.
:todo
end
@doc "Returns the text of {:reply, id, text}, the reply to request id; a reply to another request must not match."
def reply_text(_id, _reply) do
:todo
end
end Code.require_file("replay.ex", __DIR__)
ExUnit.start(seed: 0)
defmodule ReplayTest do
use ExUnit.Case
test "move reads both numbers" do
assert Replay.move("move 3 4") == {:move, 3, 4}
end
test "move reads negative and larger numbers" do
assert Replay.move("move -2 140") == {:move, -2, 140}
end
test "a line that is not a move does not match" do
assert_raise MatchError, fn -> Replay.move("jump 3 4") end
end
test "a move with one number does not match" do
assert_raise MatchError, fn -> Replay.move("move 3") end
end
test "a number with letters after it does not match" do
assert_raise MatchError, fn -> Replay.move("move 3x 4") end
end
test "reply_text returns the text of the reply to this request" do
assert Replay.reply_text(7, {:reply, 7, "door opened"}) == "door opened"
end
test "a reply to another request does not match" do
assert_raise MatchError, fn -> Replay.reply_text(7, {:reply, 8, "door opened"}) end
end
end Run elixir replay_test.exs. The starter returns :todo, so all seven tests fail, the four that expect a
MatchError included:
from session import run
# The tests against the starter, which changes nothing.
run("""
cd practice
!elixir replay_test.exs
""") Output
$ cd practice
$ elixir replay_test.exs
Running ExUnit with seed: 0, max_cases: 36
1) test move reads both numbers (ReplayTest)
replay_test.exs:7
Assertion with == failed
code: assert Replay.move("move 3 4") == {:move, 3, 4}
left: :todo
right: {:move, 3, 4}
stacktrace:
replay_test.exs:8: (test)
2) test move reads negative and larger numbers (ReplayTest)
replay_test.exs:11
Assertion with == failed
code: assert Replay.move("move -2 140") == {:move, -2, 140}
left: :todo
right: {:move, -2, 140}
stacktrace:
replay_test.exs:12: (test)
3) test a line that is not a move does not match (ReplayTest)
replay_test.exs:15
Expected exception MatchError but nothing was raised
code: assert_raise MatchError, fn -> Replay.move("jump 3 4") end
stacktrace:
replay_test.exs:16: (test)
4) test a move with one number does not match (ReplayTest)
replay_test.exs:19
Expected exception MatchError but nothing was raised
code: assert_raise MatchError, fn -> Replay.move("move 3") end
stacktrace:
replay_test.exs:20: (test)
5) test a number with letters after it does not match (ReplayTest)
replay_test.exs:23
Expected exception MatchError but nothing was raised
code: assert_raise MatchError, fn -> Replay.move("move 3x 4") end
stacktrace:
replay_test.exs:24: (test)
6) test reply_text returns the text of the reply to this request (ReplayTest)
replay_test.exs:27
Assertion with == failed
code: assert Replay.reply_text(7, {:reply, 7, "door opened"}) == "door opened"
left: :todo
right: "door opened"
stacktrace:
replay_test.exs:28: (test)
7) test a reply to another request does not match (ReplayTest)
replay_test.exs:31
Expected exception MatchError but nothing was raised
code: assert_raise MatchError, fn -> Replay.reply_text(7, {:reply, 8, "door opened"}) end
stacktrace:
replay_test.exs:32: (test)
Finished in 0.02 seconds (0.02s on load, 0.00s async, 0.00s sync)
Result: 0/7 passed
Failed: 7 tests
[exit status 2]
This output changes from run to run: ExUnit prints how long the tests took, and max_cases follows the computer's processors
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 practice_session.py
move/1 needs four matches in a row, one for each thing a good line promises: the prefix, exactly two parts after
it, and two whole numbers. If any of them fails, the line was not a move, and the MatchError is the test’s
evidence. reply_text/2 needs one match with a pinned id. A finished version passes all seven tests:
from session import run
# The same tests against finished move/1 and reply_text/2.
run("""
cd finished
cp ../practice/replay_test.exs .
elixir replay_test.exs
""") Output
$ cd finished $ cp ../practice/replay_test.exs . $ elixir replay_test.exs Running ExUnit with seed: 0, max_cases: 36 ....... Finished in 0.01 seconds (0.01s on load, 0.00s async, 0.00s sync) Result: 7 passed
This output changes from run to run: ExUnit prints how long the tests took, and max_cases follows the computer's processors
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 finished_session.py
Last comes the self-check on patterns, lists and maps, prefixes, the pin and MatchError. It opens with whichever of
those five your answers show to be weakest, and every answer is explained the moment you check it:
Practice that adapts to you
Practise this lesson
20 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up: is = assignment in Elixir? What does it do, and what is the pin operator for?
No. = is the match operator: it matches the value on its right against the pattern on its left. A name in the
pattern is bound to the value in its place, a literal must be equal, and the shapes must agree, so
{:ok, port} = {:ok, 4000} binds port to 4000, while {:ok, port} = {:error, :enoent} raises MatchError and
binds nothing. x = 1 only looks like assignment because a lone name matches anything. Elixir lets a name be bound
again, so a later x = 2 rebinds x, and the value 1 itself never changes. The pin operator ^ turns rebinding off
for one name: ^x stands for the value x already has and is compared like a literal, so after x = 2, ^x = 3
raises MatchError. Use it whenever a pattern must check a known value, such as the id of the request a reply
answers.
Two questions from code that trusts the shape of its data:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
A service starts with {:ok, text} = File.read("settings.json"). On a new server it stops at boot with a MatchError showing {:error, :eacces}. A teammate proposes rescuing the error and starting with built-in defaults. What do you recommend, and how should start-up treat a settings file that is missing or wrong?
-
Fresher to mid
A webhook handler starts with %{"event" => event, "amount" => amount} = payload, the decoded JSON from a payment partner. After the partner adds a refund event without an amount, requests fail with 500, the partner retries, and the crash logs fill with whole payloads, customer emails included. Explain each effect and redesign the handler.
Key takeaways
=matches the value on its right against the pattern on its left: names are bound, literals must be equal, and the shapes and sizes must agree, or the match raisesMatchErrorand binds nothing.- Numbers in patterns are compared strictly, so
1never matches1.0, and_or a name like_statusmatches a value you do not need. [first | rest]matches a list of one item or more and[a, b]exactly two, while%{key: v}matches any map that holds the key and%{}every map."prefix" <> resttakes a fixed prefix off the front of a string; a part of unknown size before<>is refused when the code is compiled.- A name on the left of
=is bound again whatever it held, and^namecompares with its value instead, which is how every bound variable behaves in Erlang. - A failed match is a failed assertion: let it stop the program when the failure means a bug or a broken setup, and
handle it with
casewhen users or the outside world can cause it.
Terms in this lesson
- let it crash
- The Erlang and Elixir habit of letting code fail at once when something unexpected happens, instead of rescuing every possible error, so that a supervisor can start the work again in a known state.
- match operator
- =, which matches the value on its right against the pattern on its left. When they fit, it binds the names of the pattern and returns the value; when they do not, it raises MatchError.
- MatchError
- The exception that = raises when the value on its right does not fit the pattern on its left. Its message, no match of right hand side value, is followed by the value that did not fit.
- pattern
- The left side of a match, such as {:ok, port} or [head | tail]: the shape a value must have. Literals in it must equal the parts of the value, and the names in it are bound to whatever stands in their place.
- pin operator
- ^ before a name in a pattern, as in ^x = 3. The pinned name stands for the value it already has and is compared like a literal instead of being bound again.
- rebinding
- Binding a name that already has a value to another value, as x = 2 after x = 1. Elixir allows it, and the old value does not change; Erlang does not, because there a variable is bound only once.
- underscore variable
- _ or a name that starts with _, such as _status. It matches any value and says that the value is not used; _ itself can never be read, and the compiler warns when a name like _status is.
References
- Elixir guide: Pattern matching (The Elixir Team (hexdocs.pm))
- Patterns and guards (Elixir reference) (The Elixir Team (hexdocs.pm))
- Kernel.SpecialForms (Elixir): =/2, ^/1 and <<>>/1 (The Elixir Team (hexdocs.pm))
- Kernel (Elixir): match?/2 (The Elixir Team (hexdocs.pm))
- Integer.parse/2 (Elixir) (The Elixir Team (hexdocs.pm))
- File (Elixir): read/1 and read!/1 (The Elixir Team (hexdocs.pm))
- Elixir guide: try, catch, and rescue (fail fast, let it crash) (The Elixir Team (hexdocs.pm))
- Erlang reference manual: Pattern Matching (Erlang/OTP (erlang.org))
- Erlang reference manual: Expressions (variables and the match operator) (Erlang/OTP (erlang.org))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress