Beginner

Control Flow in Elixir

Learn how to direct program flow in Elixir using if/else, case, cond, guards, pattern matching, and recursion with Docker-ready examples

Control flow is how a program decides what to do next. Most languages reach for if, for, and while to make those decisions. Elixir has if, but as a multi-paradigm functional language it leans on tools that fit immutable data far better: pattern matching, multi-clause functions, case, cond, guards, and recursion.

The biggest mental shift is that Elixir has no mutable loop counters. You can’t write i = i + 1 because data is immutable. Instead, iteration is expressed through recursion or through the Enum and comprehension constructs that wrap it. Branching, meanwhile, is often handled by matching the shape of data rather than testing it with conditionals.

In this tutorial you’ll learn how Elixir handles conditionals (if, unless, case, cond), how guards add conditions to pattern matches, and how recursion replaces the traditional loop. Every example runs as a standalone .exs script.

If, Unless, and Expressions

Elixir does have if and unless, and unlike many languages they are expressions — they return a value you can assign.

Create a file named if_else.exs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
age = 20

if age >= 18 do
  IO.puts("You are an adult")
else
  IO.puts("You are a minor")
end

# `unless` is the inverse of `if`
unless age == 0 do
  IO.puts("Age is not zero")
end

# Because `if` returns a value, you can assign its result
status = if age >= 65, do: "senior", else: "working age"
IO.puts("Status: #{status}")

Note the inline do:/else: form — handy for short, one-line conditionals.

Case: Matching on Shape

case compares a value against a series of patterns and runs the first branch that matches. This is the idiomatic replacement for the switch statement, and it does far more because each pattern can destructure data. The _ pattern is a catch-all.

Create a file named case_match.exs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
defmodule TrafficLight do
  def action(color) do
    case color do
      :red -> "Stop"
      :yellow -> "Slow down"
      :green -> "Go"
      _ -> "Unknown signal"
    end
  end
end

IO.puts(TrafficLight.action(:red))
IO.puts(TrafficLight.action(:green))
IO.puts(TrafficLight.action(:blue))

Here :red and friends are atoms — constants whose value is their own name, commonly used as labels in Elixir.

Cond and Guards: Choosing on Conditions

When you need to branch on arbitrary boolean conditions rather than match a single value, use cond — it behaves like an if/else-if chain. The first condition that evaluates to a truthy value wins, and true serves as the default branch.

Guards (when) let you attach conditions directly to function clauses, so the right clause is selected automatically without any conditional inside the body.

Create a file named cond_guards.exs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
defmodule Grade do
  # Guards pick the matching clause based on the condition
  def letter(score) when score >= 90, do: "A"
  def letter(score) when score >= 80, do: "B"
  def letter(score) when score >= 70, do: "C"
  def letter(_score), do: "F"
end

IO.puts(Grade.letter(95))
IO.puts(Grade.letter(83))
IO.puts(Grade.letter(60))

# `cond` evaluates conditions top to bottom and takes the first truthy one
temp = 30

description =
  cond do
    temp > 35 -> "Scorching"
    temp > 25 -> "Warm"
    temp > 15 -> "Mild"
    true -> "Cold"
  end

IO.puts("It is #{description}")

Recursion: Looping the Functional Way

Elixir has no for or while loop that mutates a counter. Repetition is expressed through recursion. Multi-clause functions make this clean: one clause defines the base case that stops the recursion, another does the work and calls itself with a smaller argument.

Create a file named recursion.exs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
defmodule Counter do
  # Base case: stop when we reach zero
  def count_down(0), do: IO.puts("Liftoff!")

  # Recursive case: print, then count down from one less
  def count_down(n) do
    IO.puts(n)
    count_down(n - 1)
  end
end

Counter.count_down(3)

Pattern matching on the literal 0 chooses the stopping clause automatically — no explicit if needed to check whether to continue.

Enum, Comprehensions, and With

In day-to-day code you rarely write raw recursion. The Enum module and list comprehensions wrap it for you, and the with expression chains pattern matches together, short-circuiting as soon as one fails.

Create a file named iteration.exs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Iterate over a range with Enum instead of a counter loop
Enum.each(1..3, fn i -> IO.puts("Line #{i}") end)

# A comprehension that filters: keep only even numbers
evens = for n <- 1..10, rem(n, 2) == 0, do: n
IO.puts("Evens: #{inspect(evens)}")

# `with` chains matches; the body runs only if every match succeeds
result =
  with {:ok, age} <- {:ok, 21},
       true <- age >= 18 do
    "Access granted"
  else
    _ -> "Access denied"
  end

IO.puts(result)

inspect/1 turns the list into its source representation so it prints cleanly inside a string.

Running with Docker

You can run every example without installing Elixir locally.

1
2
3
4
5
6
7
8
9
# Pull the official image
docker pull elixir:1.17-alpine

# Run each control-flow example
docker run --rm -v $(pwd):/app -w /app elixir:1.17-alpine elixir if_else.exs
docker run --rm -v $(pwd):/app -w /app elixir:1.17-alpine elixir case_match.exs
docker run --rm -v $(pwd):/app -w /app elixir:1.17-alpine elixir cond_guards.exs
docker run --rm -v $(pwd):/app -w /app elixir:1.17-alpine elixir recursion.exs
docker run --rm -v $(pwd):/app -w /app elixir:1.17-alpine elixir iteration.exs

Expected Output

Running if_else.exs:

You are an adult
Age is not zero
Status: working age

Running case_match.exs:

Stop
Go
Unknown signal

Running cond_guards.exs:

A
B
F
It is Warm

Running recursion.exs:

3
2
1
Liftoff!

Running iteration.exs:

Line 1
Line 2
Line 3
Evens: [2, 4, 6, 8, 10]
Access granted

Key Concepts

  • if/unless are expressions — they return a value, so you can assign the result directly instead of mutating a variable.
  • case matches shape — it compares a value against patterns and destructures data in the same step, replacing the traditional switch.
  • cond chains conditions — use it when branches depend on arbitrary boolean tests rather than a single value; true is the catch-all.
  • Guards (when) select clauses — attaching conditions to function clauses pushes branching into the function head, keeping bodies clean.
  • Recursion replaces loops — with no mutable counters, repetition is expressed by a function calling itself toward a base case.
  • Enum and comprehensions wrap recursion for everyday iteration and filtering over collections.
  • with short-circuits a sequence of pattern matches, running its body only when every step succeeds and falling through to else otherwise.
  • Pattern matching over conditionals — matching the structure of data (like the literal 0) often eliminates the need for explicit if checks.

Running Today

All examples can be run using Docker:

docker pull elixir:1.17-alpine
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining