Control flow in Elixir is simpler than it looks at first, but only if you separate two ideas that many languages mix together: truth-based decisions (if, cond) and pattern-based decisions (case).
Bruce says it directly in class: “With functional programming, flow of control expressions aren’t as important, but they’re convenient and make ideas clear.”
That line matters. In Elixir, you can often solve things with function heads and pattern matching alone. But if, cond, and case are still useful because they make intent obvious. The key is knowing when to use each one, and remembering one non-negotiable rule so your code does not crash at runtime.
Everything Returns a Value
Before picking a tool, keep this sentence in your head: “Everything in Elixir is an expression. An expression returns a value.”
Most of the confusion with Elixir control flow disappears once that clicks. You are not executing a branch. You are evaluating an expression and binding the result.
status = if x > 10 do
:high
else
:low
end
This also explains a common surprise: if you write an if with no else, the false branch returns nil. There is no silent failure. The expression always resolves to something.
🎯 Join Groxio's Newsletter
Weekly lessons on Elixir, system design, and AI-assisted development -- plus stories from our training and mentoring sessions.
We respect your privacy. No spam, unsubscribe anytime.
Three Tools for Three Situations
if works for simple binary conditions. If you find yourself nesting if statements, you’ve outgrown the tool.
cond is for when you have several unrelated conditions to check. It evaluates expressions top to bottom and takes the first truthy branch. Bruce shows this in class with a temperature example:
result = cond do
x > 15 -> "very hot"
x > 10 -> "a little hot"
true -> "whatever"
end
That true at the end is not optional. If no clause matches and there is no guaranteed fallback, Elixir raises a runtime error. The true clause is always truthy, so it ensures the expression always resolves.
case is the tool you’ll reach for most often. It doesn’t just check for truth. It matches against the shape of the data, letting you branch based on internal structure and extract values at the same time.
case File.read("config.json") do
{:ok, content} -> parse(content)
{:error, :enoent} -> "File missing"
{:error, reason} -> "Other error: #{inspect(reason)}"
end
The Catch-All Rule
Both cond and case can fail at runtime if nothing matches. Bruce is clear about this in class: “If there are no matching clauses, the expression can break. Make the last clause match everything.”
Treat this as a safety rule, not a style preference.
In cond, that means ending with true ->. In case, it means ending with _ ->. The underscore matches anything without binding it to a name.
This is a feature, not a limitation. Elixir forces you to be explicit about every possible state your system can enter. If you haven’t thought about a case, the crash will tell you.
Only Catch What You Can Fix
The most common mistake with case is using a generic catch-all to hide errors you haven’t thought through.
# Hides permission errors, disk failures, everything
case File.read("config.json") do
{:ok, body} -> body
_ -> "{}"
end
Instead, match only the errors you actually know how to handle:
case File.read("config.json") do
{:ok, body} -> body
{:error, :enoent} -> "{}"
end
If a permission error or disk failure occurs, this version crashes. That crash is useful information. It tells the supervisor something is fundamentally wrong with the environment, which is exactly the kind of signal Elixir’s fault-tolerance model depends on.
The rule of thumb: only catch what you can fix. Let everything else crash.
A sidebar: why if takes a keyword list
This is optional. Skip ahead if you’re already comfortable with the practical stuff above.
Bruce digs into this in a separate class. Under the hood, if in Elixir is a macro, not a keyword. It takes two arguments: a condition and a keyword list. The do: and else: blocks are atoms in that list.
You can write this:
if(x == 42, do: "yes", else: "no")
That’s the same as:
if x == 42 do
"yes"
else
"no"
end
The do...end syntax is sugar over the keyword list form. The same pattern shows up in def, if, case, and most constructs in Elixir. They’re all macros that accept keyword options as their last argument. Bruce shows in class that even def follows this exact shape.
It’s a small window into how Elixir builds the language inside the language itself.
If you connect this post to the previous ones, the progression is clean: first you learned that matching is about structure, then that tuples act as contracts. Control flow now sits on top of all that. You are not just branching. You are choosing whether truth or pattern is the right decision mechanism, and you are always returning a value.
When that clicks, control flow stops feeling like syntax and starts feeling like design.
In the next post, we’ll look at how these tools combine with functions to build clean, composable logic.
If you want to go deeper on these patterns with guided exercises and Bruce teaching live, check out the Groxio Elixir course.
📚 Master Elixir Control Flow for Production Architecture Decisions
This comes from our Learning Elixir course, where Bruce teaches the mental models behind real-world scenarios and production architecture decisions. Learn when to use truth-based branching versus pattern-based matching so your code stays explicit, predictable, and maintainable.
— Paulo & Bruce