5 min read
On this page

Case, Cond, and If

Elixir gives you three ways to branch: case, cond, and if. They look superficially similar but solve different problems. case is for pattern matching, cond is for boolean conditions, and if is for the simple binary choice. Most idiomatic Elixir code uses case first, multi-clause functions second, and if only when nothing else fits.

case: Pattern Matching as Control Flow

case evaluates an expression and matches it against patterns. The first pattern that matches wins.

case File.read("config.json") do
  {:ok, contents} ->
    Jason.decode!(contents)

  {:error, :enoent} ->
    %{}

  {:error, reason} ->
    raise "could not read config: #{inspect(reason)}"
end

This is the canonical shape of error handling in Elixir. You get a tagged tuple back, you case on it, you handle each shape. Most of the standard library and most third-party libraries follow this convention — File.read/1, Jason.decode/1, HTTPoison.get/1, Repo.insert/1 all return either {:ok, value} or {:error, reason}.

case clauses can use guards just like function clauses:

case http_status do
  status when status in 200..299 -> :success
  status when status in 400..499 -> :client_error
  status when status in 500..599 -> :server_error
  _ -> :unknown
end

The _ clause is a wildcard. If no clause matches and there is no wildcard, you get a CaseClauseError at runtime, which is rarely what you want. When in doubt, add the wildcard.

A pattern: many Elixir codebases push branching out of case and into multi-clause functions. The case above could equivalently be:

defp categorize(status) when status in 200..299, do: :success
defp categorize(status) when status in 400..499, do: :client_error
defp categorize(status) when status in 500..599, do: :server_error
defp categorize(_), do: :unknown

The function-clause form is generally preferred when the branching is reusable or large. case is preferred for branching that is tied to one specific call site.

cond: When Patterns Don't Help

cond is for when you have a sequence of boolean conditions that are not pattern matches.

cond do
  age < 13 -> :child
  age < 20 -> :teenager
  age < 65 -> :adult
  true -> :senior
end

The first clause whose condition is truthy wins. true is the conventional fallthrough — there is no else.

cond is rarer than case in real Elixir code. Most branching has structure that pattern matching can express, and structured matching scales better than boolean ladders. Reach for cond when:

  • You are comparing the same value against ranges or arithmetic conditions.
  • Multiple unrelated conditions need to be checked in order.
  • The conditions involve calls to functions, not pattern shape.
cond do
  String.contains?(email, "@admin.") -> :admin
  String.ends_with?(email, "@partner.com") -> :partner
  String.length(email) > 0 -> :user
  true -> :anonymous
end

If you find yourself writing cond clauses that are all checking the same variable's structure, that is a sign you wanted case.

if and unless

if is the simple two-branch form. It is built as a macro on top of case, but it reads like the keyword you are used to from other languages.

if user.admin? do
  render_admin_dashboard()
else
  render_user_dashboard()
end

unless is the same thing inverted. unless x is if !x.

unless Enum.empty?(errors) do
  Logger.warning("validation failed: #{inspect(errors)}")
end

unless is fine for negating a single condition, but unless x and y and not z is a brain-teaser. Most style guides recommend using unless only with simple positive conditions.

Both if and unless are expressions, not statements. They return a value.

greeting =
  if user.name do
    "Hello, #{user.name}"
  else
    "Hello, stranger"
  end

Without an else and a falsy condition, the result is nil. That is a real value you can pipe or assign, just one that is often what you wanted not to happen.

Truthy and Falsy: Only nil and false

This is one place Elixir is stricter than most dynamic languages. The only falsy values are nil and false. Everything else — including 0, "", [], %{} — is truthy.

if 0, do: "truthy"        # "truthy"
if "", do: "truthy"       # "truthy"
if [], do: "truthy"       # "truthy"
if %{}, do: "truthy"      # "truthy"
if nil, do: "truthy"      # nil
if false, do: "truthy"    # nil

If you come from JavaScript or Python, this will bite you. The empty list [] is truthy in Elixir. Checking if list is not "is this list non-empty" — it is "is this not nil." For emptiness, use Enum.empty?/1 or pattern match against [].

# Wrong
if items, do: process(items)

# Right
unless Enum.empty?(items), do: process(items)

# Better — pattern match in a function clause
def process([]), do: :nothing_to_do
def process(items), do: # ...

There is also a stricter pair: and, or, not. These require booleans on the left side and will raise on anything else.

true and "anything"   # "anything" — short-circuit returns RHS
false or "fallback"   # "fallback"
nil and "x"           # raises ArgumentError — left is not boolean

The && and || operators are the truthy/falsy versions. nil || "fallback" works. nil and "x" raises. Most code uses && and || for everyday work and reserves and/or for places where the left side is guaranteed boolean.

do/end Blocks vs Keyword Form

Elixir has two syntactic forms for case, cond, if, and friends. The do/end form is what you use most:

if x > 0 do
  :positive
else
  :nonpositive
end

The keyword form is shorter for one-liners:

if x > 0, do: :positive, else: :nonpositive

These are not different language features — they desugar to the same AST. The compiler treats do: as a keyword argument and do ... end as a block argument, but the resulting tree is identical.

The keyword form is idiomatic for trivial one-liners — single-clause function bodies, short if expressions, simple guard heads. Anything beyond that uses do/end.

# Idiomatic keyword form
def small?(n), do: n < 10

# Idiomatic block form
def categorize(n) do
  cond do
    n < 0 -> :negative
    n == 0 -> :zero
    n > 0 -> :positive
  end
end

Mixing them up is a recipe for parser confusion. if x, do: 1, else: 2 is fine. if x do: 1, else: 2 is a syntax error. The comma matters.

When to Reach for Each

A reasonable default order:

  1. Multi-clause functions when you can. They are the most idiomatic form of branching in Elixir.
  2. case when branching is tied to one call site or pattern matches a tagged result.
  3. with (covered in the next topic) when chaining multiple matches with early-exit on errors.
  4. if when there are exactly two simple branches and no pattern.
  5. cond when you have boolean conditions that are not patterns.

If you find yourself nesting case inside case inside case, that is the smell that should send you to with.

Common Pitfalls

Forgetting that [] and 0 are truthy. Treating empty collections as falsy is a hard habit to break. Use Enum.empty?/1 or pattern match instead.

Using unless with complex conditions. unless not (x or y) is unreadable. Negate the condition or rewrite as if.

Missing the wildcard clause in case. A CaseClauseError is a runtime crash that production logs love to surface. If the cases do not exhaustively cover all possibilities, add _ -> ....

Using cond for what should be a case. If you are writing cond clauses like is_atom(x) -> ...; is_binary(x) -> ..., you wanted to pattern match in a case or use multi-clause functions with guards.

Confusing and/or with &&/||. and and or require booleans and will raise on nil. && and || are the truthy/falsy versions. Use the latter when you are not certain the left side is a boolean.

Writing if with a side-effecting body and discarding the value. Elixir's if returns a value. If you ignore the return value and use it for side effects only, that is fine, but be aware that the body's last expression is the value of the entire if.

Key Takeaways

  • case matches patterns. It is the most common branching form in Elixir.
  • cond evaluates boolean conditions in order. Use it when patterns do not apply.
  • if and unless are the simple two-branch forms. They are expressions and return values.
  • Only nil and false are falsy. Empty collections, zero, and empty strings are all truthy.
  • and/or require booleans. &&/|| accept any truthy/falsy value.
  • Keyword form (do:) is for one-liners. Block form (do ... end) is for everything else.
  • Multi-clause functions are often a better fit than case for branching that recurs across the codebase.