Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Beginner
  • 20 minutes
  • Examples run with elixir 1.20.4, erlang 29.1.1 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

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:

Matches in IEx Python · match_session.py
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"):

Interactive · step by step {port, ""} = Integer.parse(…), position by position Step through two matches of one pattern. The left column is the pattern, the right column the value it is matched against.
{port, ""} = Integer.parse(…), position by position, step 7 of 7. Showing the pattern, the value ({40, "x"}), port, position 0 (40), "", position 1 ("x"), MatchError. In focus: "", position 1, MatchError.matched againstbindsequal?the pattern{port, ""}the value{40, "x"}porta name: it bindsposition 040""a literal: must be equalposition 1"x"MatchErrorit shows {40, "x"}

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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

  1. Question 1 of 2 What does this line do?

    Read the code, then choose one answer.

    {n, ""} = Integer.parse("12 ")
    Show the answer: Question 1 of 2

    Answer: It raises MatchError, because Integer.parse("12 ") returns {12, " "}

    Integer.parse/1 reads 12 and returns the rest of the text, " ", in position 1. The literal "" matches only the empty string, so the match fails and binds nothing, as {port, ""} failed on "40x".

  2. Question 2 of 2 Which of these matches succeed?

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • {_, b} = {1, 2}
    • {:ok, x} = {:ok, 1.0}

    Names match anything; literals and sizes must agree exactly. In the session {:ok, 1} failed on {:ok, 1.0} and {a, b} failed on a tuple of three elements.

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:

List and map patterns in IEx Python · collections_session.py
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

  1. Question 1 of 2 What does this line do?

    Read the code, then choose one answer.

    [a, b | _] = [10, 20]
    Show the answer: Question 1 of 2

    Answer: It matches, a is 10 and b is 20, and _ takes the empty rest

    [a, b | rest] takes two items and leaves the rest of the list, here [], in rest; with _ the rest is not bound. A list of one item would not fit, and neither would the empty list.

  2. Question 2 of 2 What does %{id: id} = %{id: 7, name: "Asha"} do?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It binds id to 7 and ignores :name

    The pattern %{name: name} matched the profile that also held :age in the same way. A key that the map does not have, such as :email, fails the match.

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.exs: commands taken apart by their prefix Elixir · commands.exs
# 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")
Running commands.exs Python · commands_session.py
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:

Interactive · step by step "deposit:" <> amount, byte by byte Step through the match of a string prefix. The left column is the pattern, the right column the string it is matched against.
"deposit:" <> amount, byte by byte, step 5 of 5. Showing "deposit:" <> amount, "deposit:250" (or "withdraw:40": no match), "deposit:", bytes 0 to 7 (w i t h d r a w), amount, bytes 8 to 10, id <> ":user", ArgumentError. In focus: id <> ":user", ArgumentError.matched againstequal?binds"deposit:" <> amounta prefix, then the rest"deposit:250"or "withdraw:40": no match"deposit:"8 bytes to comparebytes 0 to 7w i t h d r a wamounta name for the restbytes 8 to 10"250", still textid <> ":user"the unknown part firstArgumentErrorbefore any code runs

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.

Text to Binary & Hex Converter Type deposit:250 and see its 11 bytes: the 8 that the prefix compares and the 3 left for amount.

Checkpoint

Check: String prefixes with <>

  1. One question What does id hold after this line?

    Read the code, then choose one answer.

    "user:" <> id = "user:42"
    Show the answer: One question

    Answer: "42", a string

    A prefix pattern compares the bytes of its literal and binds the name after <> to the bytes that are left, as amount took "250" in commands.exs.

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:

Rebinding and the pin in IEx Python · pin_session.py
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:

Interactive · step by step x = 2 binds the name again; ^x = 3 compares Step through the lines of the session. The left column holds the name and the patterns, the right column the values they meet.
x = 2 binds the name again; ^x = 3 compares, step 4 of 4. Showing x, 1 (unchanged, no longer x), 2, ^x = 3, 3, {y, ^x} = {5, 2}, y is 5. In focus: {y, ^x} = {5, 2}, y is 5.names2 = 3? noxa name1unchanged, no longer x2another value^x = 3^x stands for 23the right side{y, ^x} = {5, 2}y binds, ^x comparesy is 5x is still 2

The drawing shows the whole scene; its 4 steps are listed below.

Every step as text
  1. x = 1. x = 1 matches the value 1 against the pattern x. A name matches anything, so x is bound to 1.
  2. 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.
  3. ^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.
  4. {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:

The same matches in the Erlang shell Python · erlang_session.py
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:

rebind.exs: two names the compiler questions Elixir · rebind.exs
# 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}")
Warnings about names Python · rebind_session.py
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

  1. Question 1 of 2 What does the last line return?

    Read the code, then choose one answer.

    x = 5
    {^x, y} = {5, 6}
    y
    Show the answer: Question 1 of 2

    Answer: 6

    A pinned name is compared like a literal while the other names of the pattern are bound, as {y, ^x} = {5, 2} bound y to 5 in the session.

  2. Question 2 of 2 id = 7 was set earlier. Then {:reply, id, text} = {:reply, 8, "hi"} runs. What happens?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The match succeeds, and id now names 8

    This is the slip of the mistake box: to check a known value, pin it, as in {:reply, ^id, text}. Without the pin the pattern accepts any id.

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:

Lines refused before they run Python · refused_session.py
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:

startup.exs: the settings as a match Elixir · startup.exs
# 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")
startup_bang.exs: the same read with File.read!/1 Elixir · startup_bang.exs
# 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")
Starting without settings, then with them Python · startup_session.py
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:

Interactive · answer and compare Match, raise, or handle the failure? Answer the two questions about the value you are about to match. The table marks the ways that fit, and why.
Can the failure happen in normal use, such as a file name a user typed or a network that is down?
Do you need the value itself, or only to know whether it fits?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Four ways to use a result that may not fit, compared on reading a settings file that is missing
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, _}:

typecheck/port_config.ex Elixir · typecheck/port_config.ex
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
A pattern that can never match, found when compiling Python · typecheck_session.py
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

  1. Question 1 of 2 A program stops with "no match of right hand side value: {:error, :eacces}". What does it tell you?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A function returned an error tuple, with the reason :eacces, where the code expected another shape

    The text after "no match of right hand side value" is the value that did not fit. For File.read/1, :eacces is one of the typical reasons the File documentation lists: missing permission to read the file.

  2. Question 2 of 2 A command-line tool reads a file whose name the user types. Which way of reading it fits?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: case File.read(name) do …, with a clause for {:error, reason} that tells the user

    Let it crash is for the unexpected, such as a settings file missing when a service starts. A file name a user typed may well be wrong, so that failure is expected and is handled with case, the next lesson.

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:

practice/replay.ex: the starter Elixir · practice/replay.ex
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
practice/replay_test.exs: the tests Elixir · practice/replay_test.exs
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:

The tests against the starter Python · practice_session.py
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:

The tests against finished move/1 and reply_text/2 Python · finished_session.py
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.

  1. Question 1 of 20 What does a successful match such as {status, body} = {:ok, "data"} return?

    Choose one answer.

    Show the answer: Question 1 of 20

    Answer: The value on the right, {:ok, "data"}

    = returns the value on its right when the pattern fits, so a match can be used inside a larger expression, and IEx prints that value.

  2. Question 2 of 20 The second line raises MatchError. What does port hold afterwards?

    Read the code, then choose one answer.

    port = 1
    {port, ""} = {2, "x"}
    Show the answer: Question 2 of 20

    Answer: 1

    The literal "" did not match "x", so the whole match failed before any name was bound, and port still names 1.

  3. Question 3 of 20 What does _ do in {_, second, _} = {:a, :b, :c}?

    Choose one answer.

    Show the answer: Question 3 of 20

    Answer: It matches any value and binds nothing

    _ matches anything and is never bound. A name such as _status also matches anything and documents what it ignores, and reading it later draws a warning.

  4. Question 4 of 20 What does this line do?

    Read the code, then choose one answer.

    {:ok, 1} = {:ok, 1.0}
    Show the answer: Question 4 of 20

    Answer: It raises MatchError

    The patterns guide says numbers in patterns perform strict comparison. The session showed this match raising MatchError with the value {:ok, 1.0}.

  5. Question 5 of 20 What are h and t after this line?

    Read the code, then choose one answer.

    [h | t] = [7]
    Show the answer: Question 5 of 20

    Answer: h is 7 and t is []

    [head | tail] matches every list of one item or more. The head is the first item and the tail the list of the rest, which may be [].

  6. Question 6 of 20 A function starts with [first | _] = list. For which list does it raise MatchError?

    Choose one answer.

    Show the answer: Question 6 of 20

    Answer: []

    Only the empty list fails [first | _]; every other list has a first item, whatever that item is.

  7. Question 7 of 20 Which pattern matches every map, whatever its keys?

    Choose one answer.

    Show the answer: Question 7 of 20

    Answer: %{}

    Maps match a subset of keys, so %{} matches all maps, while the empty tuple and the empty list match only themselves, as the patterns guide notes.

  8. Question 8 of 20 What does this line do?

    Read the code, then choose one answer.

    %{email: e} = %{name: "Mira"}
    Show the answer: Question 8 of 20

    Answer: It raises MatchError

    A map pattern needs every key it names, as %{email: email} failed on the profile in the session.

  9. Question 9 of 20 What does the second line do?

    Read the code, then choose one answer.

    "deposit:" <> amount = "deposit:250"
    amount + 1
    Show the answer: Question 9 of 20

    Answer: It raises ArithmeticError, because amount is the string "250"

    "deposit:" <> amount binds amount to the bytes after the prefix, the string "250". That is why commands.exs reads the number with Integer.parse/1 before it uses it.

  10. Question 10 of 20 Why does Elixir refuse the pattern name <> ".txt" = "notes.txt"?

    Choose one answer.

    Show the answer: Question 10 of 20

    Answer: The part before <> has no known size, so the match cannot tell where name ends

    A match on a binary works from the front, so the parts before the last one need a known size. To look at the end of a string, use String.ends_with?/2 or split the string.

  11. Question 11 of 20 What is first after this line?

    Read the code, then choose one answer.

    <<first, rest::binary>> = "ok"
    Show the answer: Question 11 of 20

    Answer: 111, the byte of the letter o

    commands.exs printed {111, "k"} for this match: one byte as a number, then every byte after it as a binary.

  12. Question 12 of 20 x = 1 runs, then x = 2. What happens?

    Choose one answer.

    Show the answer: Question 12 of 20

    Answer: x is bound again, and now names 2

    "Variables in Elixir can be rebound", in the guide's words. Erlang is the language where X = 2 fails after X = 1.

  13. Question 13 of 20 What does ^x mean on the left of =?

    Choose one answer.

    Show the answer: Question 13 of 20

    Answer: The value x has now, compared like a literal

    The Kernel.SpecialForms documentation describes ^ as accessing an already bound variable in a match.

  14. Question 14 of 20 What does this line do?

    Read the code, then choose one answer.

    {a, a} = {3, 4}
    Show the answer: Question 14 of 20

    Answer: It raises MatchError

    {a, a} = {1, 1} matched in the session and {a, a} = {1, 2} raised MatchError: a repeated name works like a check that two positions are equal.

  15. Question 15 of 20 What does the last line return?

    Read the code, then choose one answer.

    x = 3
    {x, ^x} = {8, 3}
    x
    Show the answer: Question 15 of 20

    Answer: 8

    The Kernel.SpecialForms documentation says ^x always refers to the value of x before the match. So the pin is compared with the old value, 3, and the plain x is bound to 8 once the whole match fits.

  16. Question 16 of 20 In the Erlang shell, X = 1. runs, then X = 2.. What happens, and why?

    Choose one answer.

    Show the answer: Question 16 of 20

    Answer: The second match fails, because a bound variable in Erlang is compared, as ^x is in Elixir

    Erlang's reference manual says a variable can only be bound once, which is why Elixir needs ^ for what every bound variable does in Erlang.

  17. Question 17 of 20 What does the message of a MatchError contain?

    Choose one answer.

    Show the answer: Question 17 of 20

    Answer: The value on the right of = that did not fit

    MatchError reports the value, so reading it tells you what the code received instead of what it expected.

  18. Question 18 of 20 A settings file is missing when a service starts. Which line stops with a message that names the file and says why?

    Choose one answer.

    Show the answer: Question 18 of 20

    Answer: File.read!("settings.json")

    Both the match and File.read!/1 stopped the script with exit status 1; the bang function's error said what went wrong in words.

  19. Question 19 of 20 What does the compiler say about {:ok, port} = Integer.parse(text) inside a module?

    Choose one answer.

    Show the answer: Question 19 of 20

    Answer: It warns that the pattern will never match, because Integer.parse/1 returns :error or {integer(), binary()}

    The warning names the type of the right side, so a wrong guess about a return value is found at compile time instead of as a MatchError in production.

  20. Question 20 of 20 Which of these failures should simply stop the program?

    Choose one answer.

    Show the answer: Question 20 of 20

    Answer: A settings file that ships with the service is missing when it starts

    Let it crash is for the unexpected: a failure that means a bug or a broken setup. Failures that users or the outside world cause are expected, and the next lesson's case handles them.

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.

  1. 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?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

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

    Model answer, Pro. First step and follow-ups

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 raises MatchError and binds nothing.
  • Numbers in patterns are compared strictly, so 1 never matches 1.0, and _ or a name like _status matches 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" <> rest takes 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 ^name compares 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 case when 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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.