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

case, cond, if and unless

Choose between case, cond, if and unless in Elixir: clauses and guards, what happens when nothing matches, if without else, and the 1.20 clause checks.

  • 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

  • Write case clauses with patterns and guards that branch on the shape of a value
  • Choose cond for a chain of conditions and if for a single one
  • Predict which constructs raise when nothing matches and which return nil
  • Use the value a branch returns instead of binding a name inside it
  • Read the warnings Elixir 1.20 gives about clauses that can never run

Before you start

What you’ll learn · Before you start

What you will learn

  • Write case clauses with patterns and guards that branch on the shape of a value
  • Choose cond for a chain of conditions and if for a single one
  • Predict which constructs raise when nothing matches and which return nil
  • Use the value a branch returns instead of binding a name inside it
  • Read the warnings Elixir 1.20 gives about clauses that can never run

Before you start

On this page

A payment service answers in three shapes, {:ok, receipt}, {:error, :timeout} or {:error, reason}, and each needs different code: show the receipt, try again, or say why the payment was refused. case tries patterns like those of the previous lesson against the reply, from the top, and runs the first that fits; a reply that fits none raises an error. Elixir has three more ways to branch, and they do not agree on what happens when nothing fits.

Below, you will branch on shapes with case and guard: A condition after when in a clause, as in {:ok, code} when code >= 400. Only a fixed set of expressions may appear in it, and an error inside a guard makes the clause fail instead of raising., chain conditions with cond, test one condition with if, see which of them raise and which return nil, keep the value a branch returns, read what Elixir 1.20 says about clause: One branch of a case, a cond or a function: a pattern or a condition, an optional guard, an arrow and the code to run. Clauses are tried from the top, and the first that fits runs. that can never run, and choose between them. unless gets its own note: it still works, but it is deprecated. Each run below used Elixir 1.20.4 on top of Erlang/OTP 29.1.1; a single comparison adds Elixir 1.19.6.

case runs the first clause that fits

Here is the payment service’s reply handled with case. Each clause is a pattern, an arrow and the code to run. case works out its value once, then tries the clauses from the top:

payment.exs: one message for each kind of reply Elixir · payment.exs
# A payment service answers in one of these shapes. case runs the first clause whose pattern fits the reply.
defmodule Payment do
  def message(reply) do
    case reply do
      {:ok, receipt} -> "paid, receipt #{receipt}"
      {:error, :timeout} -> "no answer: try again"
      {:error, reason} -> "refused: #{reason}"
    end
  end
end

IO.puts(Payment.message({:ok, "R-1042"}))
IO.puts(Payment.message({:error, :timeout}))
IO.puts(Payment.message({:error, :declined}))
IO.puts(Payment.message(:pending))
Running payment.exs Python · payment_session.py
from session import run

# The last reply fits no clause, so the script stops there.
run("""
!elixir payment.exs
""")

Output

$ elixir payment.exs
paid, receipt R-1042
no answer: try again
refused: declined
** (CaseClauseError) no case clause matching:

    :pending

    payment.exs:4: Payment.message/1
    payment.exs:15: (file)
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 payment_session.py

{:ok, "R-1042"} fitted the first clause, and receipt was bound to “R-1042” for the code of that clause. {:error, :timeout} skipped the first clause and fitted the second exactly. {:error, :declined} failed both and fitted the third, which takes any error and binds reason. :pending fitted none, so case raised CaseClauseError: The error a case raises when the value fits none of its clauses: no case clause matching, followed by the value. A cond whose conditions are all false or nil raises CondClauseError instead., “no case clause matching”, followed by the value; the stack trace points at line 4, the case, and at line 15, the call. Step through how the replies travel down the clauses:

Interactive · stage by stage case reply do …: the clauses tried from the top Step through three replies. Each one travels down the clauses until a pattern fits it, or falls out at the bottom.
case reply do …: the clauses tried from the top, step 6 of 6. Showing The reply (:pending), {:ok, receipt} ->, {:error, :timeout} ->, {:error, reason} ->, CaseClauseError. In focus: {:error, :timeout} ->, {:error, reason} ->.The reply:pending{:ok, receipt} ->clause 1: paid{:error, :timeout} ->clause 2: try again{:error, reason} ->clause 3: refusedCaseClauseErrorwhen no clause fits

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

Every step as text
  1. The value, once. case evaluates the expression after the word case once, here the reply {:error, :timeout}, and then tries its clauses against that value, from the top.
  2. Clause 1 does not fit. The first pattern, {:ok, receipt}, needs :ok in position 0. The reply holds :error there, so the clause does not fit, and its code does not run.
  3. Clause 2 fits. {:error, :timeout} fits exactly, so its code runs, and its value is the value of the whole case. The clauses below it are never tried.
  4. A declined payment. {:error, :declined} fails clause 1, and fails clause 2, whose atom is :timeout. Clause 3, {:error, reason}, fits any error and binds reason to :declined.
  5. No clause fits. :pending is not a tuple, so it fits no clause, and case raises CaseClauseError with the value, as the last line of payment.exs did.
  6. The order matters. Put clause 3 above clause 2 and a timeout fits {:error, reason} first, so "try again" never runs. Write the specific clauses before the general ones.

The whole case is an expression: its value is the value of the clause that ran, and message/1 returned it. Names bound in a clause, such as receipt, exist only in that clause’s code. And the order of the clauses is part of their meaning. Here are the same clauses with the last two swapped, compiled, then called with a timeout:

order/payment.ex: the general clause first Elixir · order/payment.ex
defmodule Payment do
  # The clauses of payment.exs with the last two swapped: the general clause comes first.
  def message(reply) do
    case reply do
      {:ok, receipt} -> "paid, receipt #{receipt}"
      {:error, reason} -> "refused: #{reason}"
      {:error, :timeout} -> "no answer: try again"
    end
  end
end
A clause that can never run Python · clause_order_session.py
from session import run

# The compiler warns about a clause that a clause above it always takes first; the call shows the timeout refused.
run("""
cd order
elixirc payment.ex
elixir -pa . -e "IO.puts(Payment.message({:error, :timeout}))"
""")

Output

$ cd order
$ elixirc payment.ex
    warning: this clause cannot match because a previous clause at line 6 matches the same pattern as this clause
    │
  7 │       {:error, :timeout} -> "no answer: try again"
    │                          ~
    │
    └─ payment.ex:7:26

    warning: the following clause is redundant:

        {:error, :timeout} ->

    previous clauses have already matched on the following types:

        {:ok, term()}
        {:error, term()}

    type warning found at:
    │
  7 │       {:error, :timeout} -> "no answer: try again"
    │                          ~
    │
    └─ payment.ex:7:26: Payment.message/1
$ elixir -pa . -e "IO.puts(Payment.message({:error, :timeout}))"
refused: timeout

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 clause_order_session.py

The compiler warned twice that the {:error, :timeout} clause can never match, because {:error, reason} above it takes every error first, and the call proved it: the timeout was refused instead of retried.

Key idea

case works out its value once and runs the first clause whose pattern fits, so put specific clauses above general ones; a value that fits no clause raises CaseClauseError.

Checkpoint

Check: Clauses of case, tried from the top

  1. Question 1 of 2 What does this case return?

    Read the code, then choose one answer.

    case {:error, :timeout} do
      {:error, reason} -> :refused
      {:error, :timeout} -> :retry
    end
    Show the answer: Question 1 of 2

    Answer: :refused

    This is the swapped order of order/payment.ex: the timeout was refused instead of retried, and the compiler warned that the {:error, :timeout} clause can never match. Put specific clauses above general ones.

  2. Question 2 of 2 What does a case do when its value fits none of its clauses?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It raises CaseClauseError, with the value

    case raises CaseClauseError when no clause fits. Add a catch-all clause, such as _ ->, only for values you expect and have an answer for.

Guards add conditions, and an error in one only skips the clause

A pattern checks the shape of a value; a guard checks a condition on the parts the pattern bound. It follows when, and the clause runs only if its pattern fits and its guard is true. HTTP status codes make a good case: they are all integers, so their ranges decide. The second case below shows what happens when a guard itself fails:

Guards in IEx Python · guards_session.py
from session import shell

# Guards add a condition to a clause. An error inside a guard does not raise: the clause just does not match.
shell([
    "status = 404",
    "case status do",
    "  code when code in 200..299 -> :success",
    "  code when code in 400..499 -> :client_error",
    "  code when code >= 500 -> :server_error",
    "end",
    "x = 1",
    "case x do",
    "  y when hd(y) == 1 -> :list_starting_with_1",
    "  _ -> :something_else",
    "end",
    "hd(x)",
    "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)> status = 404
404
iex(2)> case status do
iex(2)>   code when code in 200..299 -> :success
iex(2)>   code when code in 400..499 -> :client_error
iex(2)>   code when code >= 500 -> :server_error
iex(2)> end
:client_error
iex(3)> x = 1
1
iex(4)> case x do
iex(4)>   y when hd(y) == 1 -> :list_starting_with_1
iex(4)>   _ -> :something_else
iex(4)> end
:something_else
iex(5)> hd(x)
** (ArgumentError) errors were found at the given arguments:

  * 1st argument: not a nonempty list

    :erlang.hd(1)
    iex:5: (file)
iex(5)> System.halt()

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 guards_session.py

code when code in 400..499 took 404, because in with a range is allowed in a guard. In the second case, hd(y) raised an error, since hd/1 takes only a non-empty list, as the last line shows on its own. Inside the guard, the same error did not escape: the Elixir guide says that errors in guards “do not leak but simply make the guard fail”, so the first clause did not match, and _ took the value. Step through it:

Interactive · stage by stage y when hd(y) == 1: an error inside a guard Step through the case on x = 1. The guard raises an error, and the error goes no further than the guard.
y when hd(y) == 1: an error inside a guard, step 4 of 4. Showing x = 1, y when hd(y) == 1 ->, hd(1) raises, _ -> :something_else. In focus: _ -> :something_else.x = 1an integer, not a listy when hd(y) == 1 ->clause 1: y binds to 1hd(1) raisesArgumentError in the guard_ -> :something_elseclause 2: fits anythingnext clause

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

Every step as text
  1. The pattern fits. case tries clause 1. Its pattern is the name y, which fits any value, so y is bound to 1, and the guard after when decides.
  2. The guard raises. The guard calls hd(1). hd/1 raises ArgumentError for anything that is not a non-empty list, as hd(x) did on a line of its own.
  3. The guard just fails. An error inside a guard does not leave the guard. The guard counts as false, and clause 1 simply does not match, with no error shown.
  4. The next clause runs. case moves on to clause 2. _ fits anything, so the case returns :something_else, exactly as the session printed.

A guard that fails quietly is safe, but it can also hide a value you did not expect: the value moves on to the next clause without a word, as 1 did here. Only a fixed set of expressions may appear in a guard, as the patterns guide lists them: comparisons, the strict boolean operators and, or and not, arithmetic, in with a list or a range, type checks such as is_integer/1, functions on built-in types such as hd/1 and map_size/1, and map.field. A function of a module such as String is refused when the code compiles:

guard_call/name.ex Elixir · guard_call/name.ex
defmodule Name do
  # Only a fixed set of expressions may appear in a guard, and String.length/1 is not one of them.
  def check(name) do
    case name do
      n when String.length(n) > 20 -> :too_long
      _ -> :ok
    end
  end
end
A function that may not appear in a guard Python · guard_call_session.py
from session import run

run("""
cd guard_call
!elixirc name.ex
""")

Output

$ cd guard_call
$ elixirc name.ex
    error: cannot invoke remote function String.length/1 inside a guard
    │
  5 │       n when String.length(n) > 20 -> :too_long
    │                     ^
    │
    └─ name.ex:5:21: Name.check/1


== Compilation error in file name.ex ==
** (CompileError) name.ex: cannot compile module Name (errors have been logged)
    (stdlib 8.1) lists.erl:2673: :lists.mapfoldl_1/3
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 guard_call_session.py

The compiler stopped at line 5 with “cannot invoke remote function String.length/1 inside a guard”. Work such a value out before the case, or test it in the code of a clause instead.

HTTP Status Codes Reference Look up any status code a guard sorts, such as 404 or 503: what it means and why a server sends it.

Checkpoint

Check: Guards, and errors inside them

  1. Question 1 of 2 What does this case return?

    Read the code, then choose one answer.

    case [] do
      list when hd(list) == :admin -> :admin
      _ -> :guest
    end
    Show the answer: Question 1 of 2

    Answer: :guest

    The guide says errors in guards do not leak but simply make the guard fail, as y when hd(y) == 1 fell through for x = 1 in the session.

  2. Question 2 of 2 Which of these guards compile?

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • when code in 200..299
    • when is_integer(n) and n > 0

    Only a fixed set of expressions may appear in a guard: comparisons, and, or and not, arithmetic, in with a list or range, type checks and a few functions on built-in types. Work anything else out before the case.

Elixir 1.20 finds clauses that can never run

The compiler has long caught a clause hidden behind a broader pattern, as with the swapped payment clauses. Elixir 1.20’s type checker reads further: it works out types from patterns and guards, and it carries what one clause has ruled out into the clauses after it, which its changelog calls type inference across clauses and occurrence typing: Narrowing the type of a value inside each clause from what the clauses before it ruled out. Elixir 1.20 does it for case, cond and with: after a nil clause, the next clause knows the value is not nil.. Each of these two modules has a mistake:

checks/kind.ex Elixir · checks/kind.ex
defmodule Kind do
  # The first clause takes every number, so the second can never run.
  def of(x) do
    case x do
      n when is_number(n) -> :number
      n when is_integer(n) and n > 0 -> :positive_integer
      _ -> :other
    end
  end
end
checks/server_port.ex Elixir · checks/server_port.ex
defmodule ServerPort do
  # System.get_env/1 returns nil or a string. After the nil clause, value can only be a string.
  def number do
    case System.get_env("PORT") do
      nil -> 4000
      value -> value + 1
    end
  end
end
The same two files on Elixir 1.20.4 and on 1.19.6 Python · checks_session.py
from session import run, use

# Elixir 1.20.4 reads the clauses' guards and patterns while it compiles; then the same files on Elixir 1.19.6.
run("""
cd checks
elixirc -o new kind.ex
elixirc -o new server_port.ex
""")
use(erlang="28.5.0.7", elixir="1.19.6-otp-28")
run("""
elixir --version
elixirc -o old kind.ex
elixirc -o old server_port.ex
""")

Output

$ cd checks
$ elixirc -o new kind.ex
    warning: the following clause is redundant:

        n when is_integer(n) and n > 0 ->

    previous clauses have already matched on the following types:

        float() or integer()

    where "n" was given the types:

        # type: integer()
        # from: kind.ex:6:7
        n

        # type: integer()
        # from: kind.ex:6:14
        is_integer(n)

    type warning found at:
    │
  6 │       n when is_integer(n) and n > 0 -> :positive_integer
    │                                      ~
    │
    └─ kind.ex:6:38: Kind.of/1
$ elixirc -o new server_port.ex
    warning: incompatible types given to Kernel.+/2:

        value + 1

    given types:

        -binary()-, integer()

    but expected one of:

        #1
        integer(), integer()

        #2
        integer(), float()

        #3
        float(), integer()

        #4
        float(), float()

    where "value" was given the type:

        # type: binary()
        # from: server_port.ex:6:7
        value

    type warning found at:
    │
  6 │       value -> value + 1
    │                      ~
    │
    └─ server_port.ex:6:22: ServerPort.number/0
$ elixir --version
Erlang/OTP 28 [erts-16.4.0.6] [source] [64-bit] [smp:18:18] [ds:18:18:10] [async-threads:1] [jit]

Elixir 1.19.6 (compiled with Erlang/OTP 28)
$ elixirc -o old kind.ex
$ elixirc -o old server_port.ex

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 checks_session.py

In kind.ex the first clause takes every number, so the second, for positive integers, can never run. Elixir 1.20.4 called it a redundant clause: A clause that can never run, because the clauses before it already match every value it could match. The compiler warns about one, and Elixir 1.20 also finds those that only types and guards reveal. and listed the types the first clause had already matched, float() or integer(). In server_port.ex, System.get_env/1 returns nil or a string; once the nil clause has taken nil, the next clause knows that value is a binary(), so value + 1 was reported before the code ever ran. Elixir 1.19.6 compiled both files without a word.

The fixes are what the warnings point at: the specific clause first in kind.ex, and String.to_integer(value) in server_port.ex. Treat these warnings as errors in waiting: each one names a clause or an operation that cannot behave the way the code was written to behave.

Checkpoint

Check: Clauses that can never run

  1. One question Elixir 1.20 compiles a case whose first clause is n when is_number(n) and whose second is n when is_float(n). What does it report?

    Choose one answer.

    Show the answer: One question

    Answer: That the second clause is redundant, because the first already takes every number

    Elixir 1.20 infers types from patterns and guards and carries them from clause to clause. A clause whose types were all taken by earlier clauses is reported as redundant.

cond runs the first condition that is truthy

Some choices are not about shape at all. A courier sorts parcels into bands by weight, and each band is a comparison. cond takes conditions instead of patterns and runs the first one that is truthy: Counting as true in a condition. In Elixir every value is truthy except false and nil, so 0, the empty string and the empty list are all truthy. More in “Atoms, booleans, nil and comparison”:

cond in IEx Python · cond_session.py
from session import shell

# cond runs the first condition that is truthy; with none, it raises.
shell([
    "weight = 25",
    "cond do",
    "  weight <= 2 -> :small",
    "  weight <= 10 -> :medium",
    "  weight <= 20 -> :large",
    "end",
    "cond do",
    "  weight <= 2 -> :small",
    "  weight <= 10 -> :medium",
    "  weight <= 20 -> :large",
    "  true -> :freight",
    "end",
    "cond do",
    "  nil -> :first",
    "  0 -> :second",
    "end",
    "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)> weight = 25
25
iex(2)> cond do
iex(2)>   weight <= 2 -> :small
iex(2)>   weight <= 10 -> :medium
iex(2)>   weight <= 20 -> :large
iex(2)> end
** (CondClauseError) no cond clause evaluated to a truthy value
    iex:5: (file)
iex(2)> cond do
iex(2)>   weight <= 2 -> :small
iex(2)>   weight <= 10 -> :medium
iex(2)>   weight <= 20 -> :large
iex(2)>   true -> :freight
iex(2)> end
:freight
iex(3)> cond do
iex(3)>   nil -> :first
iex(3)>   0 -> :second
iex(3)> end
:second
iex(4)> System.halt()

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 cond_session.py

For a parcel of 25 kg no condition was true, so the first cond raised CondClauseError, “no cond clause evaluated to a truthy value”. The guide’s advice is a final condition that is always true, a catch-all clause: A last clause that every value fits, such as _ -> in a case or true -> in a cond. It turns a value that fits no other clause into a branch you wrote, instead of an error.: true -> :freight took every parcel the bands above it did not. Step through the second cond:

Interactive · stage by stage cond for a parcel of 25 kg Step through the conditions. cond tries them from the top and runs the first that is truthy.
cond for a parcel of 25 kg, step 5 of 5. Showing weight = 25, weight <= 2 ->, weight <= 10 ->, weight <= 20 ->, true ->. In focus: true ->.weight = 25no value to matchweight <= 2 ->:smallweight <= 10 ->:mediumweight <= 20 ->:largetrue ->:freight, the catch-alltrue

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

Every step as text
  1. Conditions, not patterns. cond has no value to match. It evaluates its conditions one by one, from the top, until one gives a truthy value, here for a parcel of 25 kg.
  2. Not small. 25 <= 2 is false, so the clause does not run, and cond moves on to the next condition.
  3. Not medium. 25 <= 10 is false as well.
  4. Not large. 25 <= 20 is false too. Without another clause, cond would run out here and raise CondClauseError, as the first cond of the session did.
  5. The catch-all. true is always truthy, so a last true -> clause takes every parcel the bands above did not: cond returns :freight.

The parcel stopped at the first truthy condition, so the order matters as it does in case: with weight <= 10 above weight <= 2, every small parcel would land in :medium. The last cond of the session shows what truthy means: anything but false and nil. The nil condition was skipped, and 0 ran its clause, although 0 counts as false in many other languages.

A cond that has no answer for some values

A cond without a final true -> raises CondClauseError for every value its conditions miss, such as a parcel over 20 kg. End it with true -> and a real answer when every value must get one, as :freight is here.

Checkpoint

Check: Chains of conditions with cond

  1. Question 1 of 2 What does this cond return?

    Read the code, then choose one answer.

    cond do
      nil -> :a
      "" -> :b
      true -> :c
    end
    Show the answer: Question 1 of 2

    Answer: :b

    In a condition, only false and nil count as false. The session showed 0 running its clause in the same way.

  2. Question 2 of 2 What does cond do when none of its conditions is truthy?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It raises CondClauseError

    End a cond with true -> and a real answer when every value must get one, as true -> :freight took the 25 kg parcel.

if tests one condition, and without else it gives nil

if is the simplest branch: one condition, a do block and an optional else block. The guide notes that it is a macro, and like cond it counts every value but false and nil as true. The block form and the one-line form, with the keywords do: and else:, mean the same:

if and unless in IEx Python · if_session.py
from session import shell

# if tests one condition; without else it gives nil when the condition is false or nil.
shell([
    "stock = 0",
    "if stock > 0 do",
    '  "in stock"',
    "else",
    '  "sold out"',
    "end",
    'label = if stock > 0, do: "in stock"',
    "label",
    'if "", do: :truthy, else: :falsy',
    'unless stock > 0, do: "sold out"',
    "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)> stock = 0
0
iex(2)> if stock > 0 do
iex(2)>   "in stock"
iex(2)> else
iex(2)>   "sold out"
iex(2)> end
"sold out"
iex(3)> label = if stock > 0, do: "in stock"
nil
iex(4)> label
nil
iex(5)> if "", do: :truthy, else: :falsy
:truthy
iex(6)> unless stock > 0, do: "sold out"
"sold out"
iex(7)> System.halt()

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 if_session.py

With 0 in stock the block form returned “sold out”. The one-line form had no else, so it returned nil, as the Kernel documentation says if does when its condition is false or nil, and label was nil: a value that string interpolation turns into an empty string without a word, and that the next step of a program may stumble over. Give every if whose value you use an else. The empty string is truthy, so if "" took its do: branch.

The last line used unless, which runs its block when the condition is false or nil. It still works on Elixir 1.20.4, but the Kernel documentation now says: “This macro is deprecated. Use if/2 instead.” Elixir 1.18 made it a soft deprecation: Marking a feature as deprecated in its documentation and changelog while the compiler stays silent about it. Elixir 1.18 soft-deprecated unless in favour of if., listed in its changelog among the deprecations with no warnings emitted, so the compiler stays silent about it for now. mix format --migrate rewrites every unless for you:

migrate/stock.ex, before the migration Elixir · migrate/stock.ex
defmodule Stock do
  def warning(stock) do
    unless stock > 0 do
      "sold out"
    end
  end

  def greeting(name) do
    unless name, do: "Hello, guest", else: "Hello, #{name}"
  end
end
mix format --migrate turns unless into if Python · migrate_session.py
from session import run

# mix format --migrate rewrites every unless as an if with the condition negated.
run("""
cd migrate
elixirc stock.ex
mix format --migrate stock.ex
cat stock.ex
""")

Output

$ cd migrate
$ elixirc stock.ex
$ mix format --migrate stock.ex
$ cat stock.ex
defmodule Stock do
  def warning(stock) do
    if not (stock > 0) do
      "sold out"
    end
  end

  def greeting(name) do
    if !name, do: "Hello, guest", else: "Hello, #{name}"
  end
end

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 migrate_session.py

elixirc compiled the module without a warning. The formatter then wrote if not (stock > 0) for the comparison, which is always a boolean, and if !name for name, which may be nil: not accepts only true and false, while ! accepts any value. The Mix documentation warns that the migration changes code and suggests running it in a commit of its own and reading the result before you keep it.

Deprecated since Elixir 1.18: unless

unless has been soft-deprecated since Elixir 1.18, so it compiles without a warning on 1.20.4. Write new code with if and the condition negated, and move old code with mix format --migrate.

Checkpoint

Check: if, else and the deprecated unless

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

    Read the code, then choose one answer.

    stock = 0
    label = if stock > 0, do: "in stock"
    label
    Show the answer: Question 1 of 2

    Answer: nil

    The Kernel documentation says if returns nil when its condition is false or nil and no else is given. Give every if whose value you use an else.

  2. Question 2 of 2 What does mix format --migrate make of unless name do, where name may be nil?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: if !name do

    The formatter negates the condition with not when it is known to be a boolean and with ! otherwise, because not raises for nil.

Bind what a branch returns, not what it assigns

Every construct of this lesson is an expression: it returns the value of the branch that ran. That matters, because a name bound inside a branch stays inside it; the guide puts it as a change that “can’t leak to outer blocks”. This script was meant to take 10 off a price of 200 for premium customers:

scope.exs: a discount that never arrives Elixir · scope.exs
# Meant to take 10 off a price of 200 for premium customers.
price = 200
premium = true
discount = 0

if premium do
  discount = 10
end

IO.puts("charged #{price - discount}")

# The fix: bind what the if returns.
discount = if premium, do: 10, else: 0
IO.puts("charged #{price - discount}")
Running scope.exs Python · scope_session.py
from session import run

run("""
elixir scope.exs
""")

Output

$ elixir scope.exs
    warning: variable "discount" is unused (there is a variable with the same name in the context, use the pin operator (^) to match on it or prefix this variable with underscore if it is not meant to be used)
    │
  7 │   discount = 10
    │   ~
    │
    └─ scope.exs:7:3

charged 200
charged 190

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 scope_session.py

The first charge was 200. discount = 10 inside the if bound a new discount that lived only in the do block, and the compiler said so before the script ran: that variable is unused, and “there is a variable with the same name in the context”. The outer discount stayed 0. The warning also suggests the pin, but ^discount = 10 would only compare 0 with 10 and raise MatchError. What the script needs is the value of the if:

Interactive · step by step discount = 10 inside an if, and the fix Step through scope.exs. The left column is the script itself, the right column what happens inside the if.
discount = 10 inside an if, and the fix, step 4 of 4. Showing discount, if premium do … end, discount in the block (gone when the block ended), price - discount, discount = if …, price - discount. In focus: discount = if …, price - discount.binds insidereadreaddiscountbound to 0if premium do … enda block of its owndiscount in the blockgone when the block endedprice - discount200 - 0 = 200discount = if …the value of the if: 10price - discount200 - 10 = 190

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

Every step as text
  1. discount = 0. discount = 0 binds discount, outside every block, to 0. price is 200 and premium is true.
  2. Inside the if. premium is true, so the if runs its do block. discount = 10 there binds a new discount that exists only in that block, which is why the compiler warned that it is unused.
  3. The block ends. When the block ends, its discount goes with it. The outer discount was never changed, so price - discount is 200 - 0, and the script charged 200.
  4. Bind what the if returns. The fix binds the outer name to the value of the if: if premium, do: 10, else: 0 returns 10, so the second charge was 200 - 10 = 190.

discount = if premium, do: 10, else: 0 binds the outer name to what the if returns, and the second charge was 190. The same holds for case and cond: bind the whole construct, and let each branch end with the value it stands for.

Key idea

Bind the value a branch returns, as in discount = if premium, do: 10, else: 0; a name bound inside a branch is gone when the branch ends.

Checkpoint

Check: Binding the value of a branch

  1. One question What does the last line return?

    Read the code, then choose one answer.

    total = 100
    if true do
      total = 90
    end
    total
    Show the answer: One question

    Answer: 100

    This is scope.exs again: the compiler warns that the inner total is unused, and the fix is total = if true, do: 90, else: total.

Choose by what you are testing

The constructs overlap: a case on true and false can do what an if does, and cond can test what guards test. Choose the one that says what decides the branch, and decide what should happen to a value that fits no branch. Answer the two questions about the branch you are writing:

Interactive · answer and compare case, cond, if or unless? Answer the two questions about the branch you are writing. The table marks the constructs that fit, and why.
What decides which branch runs?
Can a value that fits none of your branches turn up in normal use?

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

Four ways to branch in Elixir, against what they test, what happens when nothing fits and their status in Elixir 1.20
Construct The shape of data A chain of conditions When nothing fits In Elixir 1.20
case value do … Decides by: patterns and guards, on one value Good patterns and guards: {:ok, receipt}, [head | _], %{name: n} Mixed possible with guards on clauses that match anything Mixed raises CaseClauseError; add _ -> on purpose Good current
cond do … Decides by: conditions, from the top Poor conditions only: no patterns, no names bound Good one condition a clause, the first truthy one wins Mixed raises CondClauseError; end with true -> Good current
if condition do … else … end Decides by: one condition Poor one truthy test Poor nested ifs grow hard to read Mixed returns nil without else, a quiet trap Good current
unless condition do … Decides by: one condition, the other way round Poor one falsy test Poor one test only Mixed returns nil without else Poor deprecated since Elixir 1.18: use if
How the answers decide
  • “What decides which branch runs?” The shape of one value, such as {:ok, x}: rules out cond do …, if condition do … else … end and unless condition do …. Only case can match a pattern, bind the parts it names and check them with guards.
  • “What decides which branch runs?” Several separate conditions, tried in order: rules out if condition do … else … end and unless condition do … and adds a caveat to case value do …. A case on true or on a name with guards works, but cond says the intent: one condition a clause, tried in order. Nested ifs hide the order.
  • “What decides which branch runs?” A single yes-or-no condition: adds a caveat to case value do … and cond do …. They work, but one condition with two outcomes reads most plainly as if … else.
  • “What decides which branch runs?” A single yes-or-no condition: rules out unless condition do …. unless is deprecated since Elixir 1.18: write if with the condition negated, as mix format --migrate does.
  • “Can a value that fits none of your branches turn up in normal use?” Yes, and it needs an answer of its own: adds a caveat to case value do …, cond do … and if condition do … else … end. Give that value its branch: a catch-all clause, _ -> or true ->, or the else of an if, with code that handles it.
  • “Can a value that fits none of your branches turn up in normal use?” No, it would mean a bug: adds a caveat to if condition do … else … end. Leave the catch-all out, so CaseClauseError or CondClauseError stops on a value that should never come. An if without else would hand on nil instead.

In short: case when the shape of one value decides, with guards for conditions on its parts; cond for a chain of separate conditions, ending with true -> when every value must get an answer; if with an else for a single condition; and if with the condition negated where older code says unless. Leave the catch-all out when a value that fits nothing would mean a bug, so that the error stops it. When branching is a function’s whole job, several clauses of the function itself do the work of one case, as the next module shows.

Checkpoint

Check: Choosing case, cond or if

  1. Question 1 of 2 A function receives {:ok, user}, {:error, :not_found} or {:error, :banned}, and each needs different code. Which construct fits?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: case, with a clause for each shape

    When the shape of one value decides, case says it directly: one clause for each shape, in order from specific to general.

  2. Question 2 of 2 A thermostat chooses :heat below 18 degrees, :off from 18 to 24 and :cool above 24. Which code says it most plainly?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: cond do t < 18 -> :heat; t <= 24 -> :off; true -> :cool end

    A chain of separate conditions is what cond is for. A case with guards would also work, but cond says the intent: one condition a clause, the first truthy one wins.

Practise: letters for marks

Write Grade.letter/1, which turns a percentage, an integer or a float, into a letter with cond: {:ok, "A"} for 90 and above, “B” from 75, “C” from 60, “D” from 40 and “F” below that, or {:error, :out_of_range} below 0 or above 100. Then write Grade.describe/1 with case: a mark is {:ok, percent} or {:error, reason}, and it becomes a line such as “92: A”, “120: out of range” or “no mark (absent)”. Copy the two files into an empty folder:

practice/grade.ex: the starter Elixir · practice/grade.ex
defmodule Grade do
  @moduledoc "Letters for percentages, and a line about a student's mark."

  @doc """
  The letter for a percentage from 0 to 100, an integer or a float: {:ok, "A"} for 90 and above,
  "B" from 75, "C" from 60, "D" from 40 and "F" below 40; {:error, :out_of_range} below 0 or above 100.
  """
  def letter(_percent) do
    # Drop the underscore once you use the argument.
    :todo
  end

  @doc """
  A line about a mark, which is {:ok, percent} or {:error, reason}: "92: A",
  "120: out of range" or "no mark (absent)".
  """
  def describe(_mark) do
    :todo
  end
end
practice/grade_test.exs: the tests Elixir · practice/grade_test.exs
Code.require_file("grade.ex", __DIR__)
ExUnit.start(seed: 0)

defmodule GradeTest do
  use ExUnit.Case

  test "90 and above is an A" do
    assert Grade.letter(90) == {:ok, "A"}
    assert Grade.letter(100) == {:ok, "A"}
  end

  test "just below a boundary takes the lower letter" do
    assert Grade.letter(89.5) == {:ok, "B"}
    assert Grade.letter(39) == {:ok, "F"}
  end

  test "each band has its letter" do
    assert Grade.letter(75) == {:ok, "B"}
    assert Grade.letter(60) == {:ok, "C"}
    assert Grade.letter(40.0) == {:ok, "D"}
    assert Grade.letter(0) == {:ok, "F"}
  end

  test "a percentage outside 0 to 100 is an error, not a letter" do
    assert Grade.letter(101) == {:error, :out_of_range}
    assert Grade.letter(-1) == {:error, :out_of_range}
  end

  test "describe gives the letter of a mark" do
    assert Grade.describe({:ok, 92}) == "92: A"
    assert Grade.describe({:ok, 59.5}) == "59.5: D"
  end

  test "describe says when a mark is out of range" do
    assert Grade.describe({:ok, 120}) == "120: out of range"
  end

  test "describe gives the reason when there is no mark" do
    assert Grade.describe({:error, :absent}) == "no mark (absent)"
  end
end

Run elixir grade_test.exs. The starter returns :todo, so all seven tests fail:

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 grade_test.exs
""")

Output

$ cd practice
$ elixir grade_test.exs
Running ExUnit with seed: 0, max_cases: 36



  1) test 90 and above is an A (GradeTest)
     grade_test.exs:7
     Assertion with == failed
     code:  assert Grade.letter(90) == {:ok, "A"}
     left:  :todo
     right: {:ok, "A"}
     stacktrace:
       grade_test.exs:8: (test)



  2) test just below a boundary takes the lower letter (GradeTest)
     grade_test.exs:12
     Assertion with == failed
     code:  assert Grade.letter(89.5) == {:ok, "B"}
     left:  :todo
     right: {:ok, "B"}
     stacktrace:
       grade_test.exs:13: (test)



  3) test each band has its letter (GradeTest)
     grade_test.exs:17
     Assertion with == failed
     code:  assert Grade.letter(75) == {:ok, "B"}
     left:  :todo
     right: {:ok, "B"}
     stacktrace:
       grade_test.exs:18: (test)



  4) test a percentage outside 0 to 100 is an error, not a letter (GradeTest)
     grade_test.exs:24
     Assertion with == failed
     code:  assert Grade.letter(101) == {:error, :out_of_range}
     left:  :todo
     right: {:error, :out_of_range}
     stacktrace:
       grade_test.exs:25: (test)



  5) test describe gives the letter of a mark (GradeTest)
     grade_test.exs:29
     Assertion with == failed
     code:  assert Grade.describe({:ok, 92}) == "92: A"
     left:  :todo
     right: "92: A"
     stacktrace:
       grade_test.exs:30: (test)



  6) test describe says when a mark is out of range (GradeTest)
     grade_test.exs:34
     Assertion with == failed
     code:  assert Grade.describe({:ok, 120}) == "120: out of range"
     left:  :todo
     right: "120: out of range"
     stacktrace:
       grade_test.exs:35: (test)



  7) test describe gives the reason when there is no mark (GradeTest)
     grade_test.exs:38
     Assertion with == failed
     code:  assert Grade.describe({:error, :absent}) == "no mark (absent)"
     left:  :todo
     right: "no mark (absent)"
     stacktrace:
       grade_test.exs:39: (test)


Finished in 0.03 seconds (0.03s 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

For letter/1, put the range check first, percent < 0 or percent > 100, then the bands from the top down, and end with true -> for “F”, so every percentage in range gets a letter. For describe/1, let one clause take {:ok, percent} and ask letter/1, branching on its answer, and another take {:error, reason}. Keeping the range rule in letter/1 alone means it is written once. A finished version passes all seven tests:

The tests against finished letter/1 and describe/1 Python · finished_session.py
from session import run

# The same tests against finished letter/1 and describe/1.
run("""
cd finished
cp ../practice/grade_test.exs .
elixir grade_test.exs
""")

Output

$ cd finished
$ cp ../practice/grade_test.exs .
$ elixir grade_test.exs
Running ExUnit with seed: 0, max_cases: 36

.......
Finished in 0.02 seconds (0.02s 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

Now the self-check of this lesson. It starts where your answers are weakest, moves between the seven ideas above, and explains every answer the moment you give it:

Practice that adapts to you

Practise this lesson

20 questions about 7 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 this case return?

    Read the code, then choose one answer.

    case {:ok, 3} do
      {:ok, n} -> n * 2
      _ -> 0
    end
    Show the answer: Question 1 of 20

    Answer: 6

    A case is an expression: its value is the value of the clause that ran.

  2. Question 2 of 20 A case ran its clause {:ok, receipt} -> …. Can the code after the case read receipt?

    Choose one answer.

    Show the answer: Question 2 of 20

    Answer: No, a name bound in a clause exists only in that clause's code

    To use something a clause found, return it from the clause and bind the result of the whole case.

  3. Question 3 of 20 A case handles {:ok, _} and {:error, _}. The value :pending turns up now and then, and it means "ask again later". What do you add?

    Choose one answer.

    Show the answer: Question 3 of 20

    Answer: A clause for :pending with the code that asks again later

    Give every expected value its own clause, and leave the values that mean a bug to CaseClauseError.

  4. Question 4 of 20 When does the clause {:ok, code} when code >= 400 -> … run?

    Choose one answer.

    Show the answer: Question 4 of 20

    Answer: When the value is a two-element tuple that starts with :ok and its second element is 400 or more

    A clause runs when its pattern fits and its guard is true; the guard can use the names the pattern bound.

  5. Question 5 of 20 What does this case return?

    Read the code, then choose one answer.

    case 250 do
      n when n in 200..299 -> :success
      _ -> :other
    end
    Show the answer: Question 5 of 20

    Answer: :success

    The session sorted 404 into :client_error with code when code in 400..499 in the same way.

  6. Question 6 of 20 What does this case return?

    Read the code, then choose one answer.

    case {:ok, %{name: "Ravi"}} do
      {:ok, u} when u.age >= 18 -> :adult
      {:ok, _} -> :unknown_age
    end
    Show the answer: Question 6 of 20

    Answer: :unknown_age

    An error in a guard is silent: here a missing key turned into "unknown age" without a word, which is safe only if that is what a missing key should mean.

  7. Question 7 of 20 In case System.get_env("PORT") do nil -> 4000; value -> … end, which type does Elixir 1.20 give value in the second clause?

    Choose one answer.

    Show the answer: Question 7 of 20

    Answer: binary(), because the nil clause took the only other possibility

    This is occurrence typing: the warning about value + 1 said "value" was given the type binary(), from the clause on line 6 of server_port.ex.

  8. Question 8 of 20 Which compilers reported the redundant clause in kind.ex?

    Choose one answer.

    Show the answer: Question 8 of 20

    Answer: Elixir 1.20.4 only; 1.19.6 compiled it without a word

    Type inference across clauses and the warning on redundant clauses are new in Elixir 1.20, as its changelog lists them.

  9. Question 9 of 20 How do you fix kind.ex, so that positive integers get :positive_integer?

    Choose one answer.

    Show the answer: Question 9 of 20

    Answer: Put the clause n when is_integer(n) and n > 0 above the clause n when is_number(n)

    Clauses are tried from the top, so specific before general. The warning disappears once no clause is hidden behind another.

  10. Question 10 of 20 Which clause of a cond runs?

    Choose one answer.

    Show the answer: Question 10 of 20

    Answer: The first one whose condition is truthy

    The guide describes cond as finding the first condition that does not evaluate to nil or false.

  11. Question 11 of 20 What does this cond return for a parcel of 1 kg?

    Read the code, then choose one answer.

    weight = 1
    cond do
      weight <= 10 -> :medium
      weight <= 2 -> :small
      true -> :freight
    end
    Show the answer: Question 11 of 20

    Answer: :medium

    Put the narrowest condition first: with weight <= 2 above weight <= 10, the 1 kg parcel is :small.

  12. Question 12 of 20 What does this cond return?

    Read the code, then choose one answer.

    cond do
      0 -> :zero
      true -> :other
    end
    Show the answer: Question 12 of 20

    Answer: :zero

    The cond session ran its 0 clause after skipping nil, which is how every condition in Elixir is read.

  13. Question 13 of 20 Which values make if run its else branch?

    Choose one answer.

    Show the answer: Question 13 of 20

    Answer: false and nil only

    if, cond, && and || all read conditions this way: false and nil are falsy, everything else is truthy.

  14. Question 14 of 20 Which code means the same as if ready, do: :go, else: :wait?

    Choose one answer.

    Show the answer: Question 14 of 20

    Answer: if ready do :go else :wait end, written over several lines

    The keyword list after if, with do: and else:, is another way to write the do and else blocks.

  15. Question 15 of 20 What happens when Elixir 1.20.4 compiles code that uses unless?

    Choose one answer.

    Show the answer: Question 15 of 20

    Answer: It compiles without a warning, but unless is deprecated, and the documentation says to use if

    Write new code with if and the condition negated, and move old code with mix format --migrate, in a commit of its own.

  16. Question 16 of 20 Why did discount = 10 inside the if leave the discount outside it at 0?

    Choose one answer.

    Show the answer: Question 16 of 20

    Answer: A name bound inside a branch exists only inside that branch

    The compiler's warning said the inner discount was unused. Bind what the if returns instead.

  17. Question 17 of 20 Which line gives premium customers the discount the script meant?

    Choose one answer.

    Show the answer: Question 17 of 20

    Answer: discount = if premium, do: 10, else: 0

    Every branch is an expression; give the if an else and bind its value.

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

    Read the code, then choose one answer.

    status = :new
    case :paid do
      :paid -> status = :done
      _ -> :ok
    end
    status
    Show the answer: Question 18 of 20

    Answer: :new

    The rule is the same for case, cond and if: write status = case :paid do :paid -> :done; _ -> status end.

  19. Question 19 of 20 A page shows "Sign out" to a signed-in user and "Sign in" to everyone else. Which code says it most plainly?

    Choose one answer.

    Show the answer: Question 19 of 20

    Answer: if signed_in, do: "Sign out", else: "Sign in"

    Choose the construct that says what decides: here a single condition, so if with an else.

  20. Question 20 of 20 A cond sorts parcels into bands up to 1,000 kg, and a heavier parcel would mean a data error upstream. Should the cond end with true ->?

    Choose one answer.

    Show the answer: Question 20 of 20

    Answer: No; without a catch-all, CondClauseError stops the bad value where it appears

    Add a catch-all for values you expect and can answer; leave it out when a value that fits nothing would mean a bug, in the let-it-crash spirit of the previous lesson.

Interview questions

Warm-up: when do you use case, when cond and when if in Elixir?

Use case when the shape of one value decides: it matches the value against patterns from the top and runs the first clause that fits, so {:ok, receipt} and {:error, reason} get different code, and guards add conditions on the parts a pattern bound, such as code when code in 400..499. Use cond for a chain of separate conditions that are not about one value’s shape, such as weight bands; it runs the first condition that is truthy. Use if for a single condition, with an else whenever you use its value, since without one it returns nil. Know what each does when nothing fits: case raises CaseClauseError and cond raises CondClauseError, so add _ -> or true -> only for values you expect. unless is deprecated since Elixir 1.18; write if with the condition negated.

Warm-up: what is a guard in Elixir, and what may it contain?

A guard is a condition after when in a clause, as in {:ok, code} when code >= 400. The clause runs only if its pattern fits and its guard is true, and the guard can use the names the pattern bound. Only a fixed set of expressions may appear in it: comparisons, the strict and, or and not, arithmetic, in with a list or a range, type checks such as is_integer/1, functions on built-in types such as hd/1 and map_size/1, and map.field. &&, || and ! are refused because they accept any value, and so is a call such as String.length/1. An error inside a guard does not escape: the guard fails, and the next clause is tried, so a value of the wrong kind can slip past without a word.

Two questions from code that branches on data it receives:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    An order router has the clauses {:ok, order} when length(order.items) > 0 -> ship(order) and {:ok, _order} -> :empty_order. Orders from an old import, whose items field is nil, have been reported as empty for weeks, and nothing ever raised. Explain why, and rewrite the clauses.

    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

    An HTTP client returns {:ok, %{status: status}} or {:error, reason}. Write the case that decides whether to retry: which statuses and errors are worth another try, in what order the clauses go, and what happens to a status or a reason you did not expect.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • case works out its value once and runs the first clause whose pattern fits, so specific clauses go above general ones, and a value that fits no clause raises CaseClauseError.
  • A guard adds a condition after when; only a fixed set of expressions may appear in it, and an error inside a guard makes the clause fail instead of raising.
  • Elixir 1.20 reports clauses that can never run, from patterns, guards and types, and narrows a value’s type from the clauses before it, where Elixir 1.19 said nothing.
  • cond runs the first truthy condition and raises CondClauseError when none is, so end it with true -> when every value needs an answer.
  • if returns nil without an else, and unless is deprecated since Elixir 1.18 in favour of if with the condition negated, which mix format --migrate writes for you.
  • Every branch is an expression: bind the value it returns, because a name bound inside a branch is gone when it ends.

Terms in this lesson

CaseClauseError
The error a case raises when the value fits none of its clauses: no case clause matching, followed by the value. A cond whose conditions are all false or nil raises CondClauseError instead.
catch-all clause
A last clause that every value fits, such as _ -> in a case or true -> in a cond. It turns a value that fits no other clause into a branch you wrote, instead of an error.
clause
One branch of a case, a cond or a function: a pattern or a condition, an optional guard, an arrow and the code to run. Clauses are tried from the top, and the first that fits runs.
guard
A condition after when in a clause, as in {:ok, code} when code >= 400. Only a fixed set of expressions may appear in it, and an error inside a guard makes the clause fail instead of raising.
occurrence typing
Narrowing the type of a value inside each clause from what the clauses before it ruled out. Elixir 1.20 does it for case, cond and with: after a nil clause, the next clause knows the value is not nil.
redundant clause
A clause that can never run, because the clauses before it already match every value it could match. The compiler warns about one, and Elixir 1.20 also finds those that only types and guards reveal.
soft deprecation
Marking a feature as deprecated in its documentation and changelog while the compiler stays silent about it. Elixir 1.18 soft-deprecated unless in favour of if.

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.