Pattern matching is one of the first things that feels genuinely different when coming to Elixir from a more imperative background. Once it clicks, it changes how you think about writing code.
The Basics
In Elixir, = is the match operator, not assignment:
{:ok, value} = {:ok, 42}
# value is now 42
{:ok, value} = {:error, "oops"}
# ** (MatchError) no match of right hand side value: {:error, "oops"}
You're not assigning — you're asserting a shape and binding names to parts of it simultaneously.
In Function Heads
This becomes especially powerful in function definitions:
defmodule Greeter do
def greet(%{name: name, role: :admin}) do
"Welcome, #{name}. You have admin access."
end
def greet(%{name: name}) do
"Hello, #{name}."
end
def greet(_), do: "Hello, stranger."
end
Each clause handles a different shape of input. No conditionals, no if role == :admin. The code reads like a specification.
With case and with
Pattern matching shines in case expressions when handling multiple outcomes:
case fetch_user(id) do
{:ok, %{role: :admin} = user} -> render_admin(user)
{:ok, user} -> render_user(user)
{:error, :not_found} -> send_404()
{:error, reason} -> log_and_fail(reason)
end
And with chains lets you sequence operations that might fail, only continuing when each step matches:
with {:ok, user} <- fetch_user(id),
{:ok, account} <- fetch_account(user.account_id),
:ok <- check_permissions(user, account) do
render_dashboard(user, account)
else
{:error, :not_found} -> send_404()
{:error, :forbidden} -> send_403()
end
Why It Matters
Pattern matching makes the shape of your data explicit in the code itself. It forces you to think about failure cases at the same time as success cases. And it eliminates entire categories of defensive nil checks that accumulate in codebases that lack it.
Once you've worked with it for a while, code without pattern matching starts to feel like working with one hand tied behind your back.