100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Pattern Matching in Elixir

How Elixir's = operator performs structural pattern matching rather than simple assignment, and how that idea powers destructuring, function clauses, and control flow.

FoundationsBeginner9 min readJul 10, 2026
Analogies

Pattern Matching in Elixir

In most languages, = means assignment: the right side's value is stored into the variable on the left. In Elixir, = is the match operator, it attempts to make the left-hand pattern structurally match the right-hand value, binding any unbound variables along the way. If the shapes or literal values don't line up, Elixir raises a MatchError at runtime instead of silently proceeding. This single idea, that = checks structure rather than blindly overwriting, is what makes destructuring, function clauses, case, and with all work the same underlying way throughout the language.

🏏

Cricket analogy: The match operator is like a third umpire review, it doesn't just record whatever the fielder claims, it checks whether the claimed shape, out or not out, actually matches the replay evidence, and only binds the final decision if it fits.

Destructuring Lists and Tuples

Because = matches structure, it can pull values out of compound data in one step: {status, message} = {:ok, "saved"} binds status to :ok and message to "saved" simultaneously, and [first | rest] = [1, 2, 3] binds first to 1 and rest to [2, 3]. Elixir's idiomatic {:ok, result} and {:error, reason} tuples exist specifically so callers can pattern match on the first element to branch on success or failure. When you need a pattern to match against a variable's current value instead of rebinding it, the pin operator, ^variable, forces that comparison, which is essential inside functions where you don't want an existing variable silently overwritten.

🏏

Cricket analogy: Matching {:ok, result} versus {:error, reason} is like a DRS review returning either {:umpires_call, "not out"} or {:out, "lbw"}, the fielding side immediately branches its reaction based on which tagged outcome comes back.

elixir
iex> {status, message} = {:ok, "File saved"}
iex> status
:ok

iex> [first | rest] = [10, 20, 30]
iex> first
10
iex> rest
[20, 30]

iex> expected = 5
iex> ^expected = 5
5
iex> ^expected = 6
** (MatchError) no match of right hand side value: 6

Because = is a match, not an assignment, 1 = x is valid Elixir as long as x is already bound to 1, the reverse of what most languages allow. This makes it a handy way to assert a value mid-pipeline: some_function(x) |> (fn {:ok, result} = _tagged -> result end).()

Pattern Matching in Function Heads

Elixir functions can be defined as several clauses that share a name and arity but differ in their argument patterns, and Elixir tries each clause's pattern against the call's arguments from top to bottom, running the first one that matches. This replaces long if/else or switch chains found in other languages: def handle({:ok, data}), do: ... and def handle({:error, reason}), do: ... are two completely separate clauses, and the runtime automatically dispatches to the correct one based on the shape of whatever tuple is passed in. Guard clauses, added with when, layer extra boolean conditions onto a pattern, for example def charge(amount) when amount > 0, letting a clause require both a shape match and a runtime condition before it's selected.

🏏

Cricket analogy: Multiple function clauses matching different patterns is like a bowling coach having a separate drill for a yorker, a bouncer, and a googly, the right drill is selected automatically based on which delivery type is being practiced.

elixir
defmodule Parser do
  def handle({:ok, data}), do: "Got: #{data}"
  def handle({:error, reason}), do: "Failed: #{reason}"

  def charge(amount) when amount > 0 do
    {:ok, amount}
  end

  def charge(_amount), do: {:error, :invalid_amount}
end

iex> Parser.handle({:ok, "hello"})
"Got: hello"
iex> Parser.handle({:error, :not_found})
"Failed: not_found"
iex> Parser.charge(-5)
{:error, :invalid_amount}

Pattern Matching in case, cond, and with

The case expression tests a single value against a series of patterns in order and executes the body of the first branch that matches, making it the natural way to branch on the shape of a value returned by another function. When a sequence of operations must each succeed before moving to the next, nesting case inside case quickly becomes unreadable, which is exactly the problem the with special form solves: it chains several <- match steps in a flat block and jumps straight to an else clause the moment any single step fails to match, without needing to unwind manually through several layers of nested case statements.

🏏

Cricket analogy: with chaining multiple match steps is like a DRS appeal that must pass several sequential checks, a no-ball check, then edge detection, then ball tracking, if any single check fails, the whole review stops right there and reverts to the on-field decision.

elixir
def process(input) do
  case Integer.parse(input) do
    {number, ""} -> {:ok, number}
    {_number, _rest} -> {:error, :trailing_characters}
    :error -> {:error, :not_a_number}
  end
end

def checkout(cart_id) do
  with {:ok, cart} <- fetch_cart(cart_id),
       {:ok, total} <- calculate_total(cart),
       {:ok, receipt} <- charge_card(total) do
    {:ok, receipt}
  else
    {:error, reason} -> {:error, reason}
  end
end

Without the pin operator ^, a variable on the left side of = always rebinds, even if it already has a value: x = 5 followed by x = 10 quietly rebinds x to 10 rather than raising a MatchError. Only ^x = 10 would fail while x is still bound to 5, which is why the pin operator matters most inside function clauses matching against arguments that were bound earlier in the same scope.

  • The = operator in Elixir is the match operator: it makes the left pattern match the right value, binding unbound variables.
  • If the shapes don't align, Elixir raises a MatchError at runtime.
  • The pin operator ^ forces a variable to be matched against its current value instead of being rebound.
  • [head | tail] destructures lists; {a, b} destructures tuples by position.
  • Function definitions can have multiple clauses distinguished purely by argument patterns, dispatched top to bottom.
  • case tests a value against a series of patterns, running the first branch that matches.
  • with chains multiple pattern matches, short-circuiting to an else branch on the first failure.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#ElixirStudyNotes#PatternMatchingInElixir#Pattern#Matching#Elixir#Destructuring#StudyNotes#SkillVeris#ExamPrep