# Control Flow
Elixir offers four control flow structures: `if/unless`, `case`, `cond`, and `with`.
## `if/2` and `unless/2`
If-else works as you'd expect. If your condition evaluates to `truthy` (not `nil` or `false`), the code block is executed and the result is returned. If the condition evaluates to `falsy` (`nil` or `false`), the `else` block is executed if it exists; otherwise, `nil` is returned.
The reverse form of `if/2` is `unless/2`. If your `unless` condition evaluates to `truthy`, the `else` block is executed. If the condition is `falsy`, the first block is executed. It is common to choose `if` over `unless` because it doesn't make your brain hurt, unless you want to execute a side effect or throw an exception.
```elixir
age = 17
result =
if age >= 18 do
:adult
else
:minor
end
IO.inspect(result, label: 1)
unless age >= 18 do
raise "A minor tries to access the system!"
end
```
<!-- livebook:{"output":true} -->
```
1: :minor
** (RuntimeError) A minor tries to access the system!
```
## `case/2`
The `case` statement is similar to `switch` in other languages. It allows you to state multiple clauses and tries to match your conditional against each clause.
```elixir
active_status = :active
fun =
fn value ->
case value do
%{status: ^active_status} -> :active
%{status: _other_status} -> :inactive
%{age: nil} -> :age_missing
%{age: age} when is_integer(age) and age >= 18 -> :adult
%{age: age} when is_integer(age) -> :minor
# An optional fallback which always matches.
# Without it, the case statement would raise an exception if no clause matches.
_ -> :default
end
end
fun.(%{status: :active}) # => :active
fun.(%{status: :foobar}) # => :inactive
fun.(%{age: nil}) # => :age_missing
fun.(%{age: 18}) # => :adult
fun.(%{age: 17}) # => :minor
fun.(nil) # => :default
```
## `cond/1`
The `cond` construct allows you to evaluate multiple clauses and return from the first that evaluates to `truthy`. It is especially useful if you need to execute a function in a clause.
```elixir
adult? = fn age -> age >= 18 end
fun =
fn value ->
cond do
value == :active -> :active
is_integer(value) && adult?.(value) -> :adult
is_integer(value) -> :minor
# The optional fallback clause must always evaluate to a truthy value.
# If you don't give this and no other clause matches, Elixir raises an exception.
true -> :default
end
end
fun.(:active) # => :active
fun.(18) # => :adult
fun.(17) # => :minor
fun.(nil) # => :default
```
## `with/1`
The `with/1` statement is useful to return early from a sequence of steps if one step fails. If you find yourself writing nested if-else or case statements, you probably want to use `with/1` instead.
```elixir
adult? = fn age -> age >= 18 end
details = %{name: "Peter", age: 32}
# Instead of writing nested if- or case-statements like this:
check_access = fn details ->
case details do
%{age: age} ->
if adult?.(age) do
:ok
else
{:error, :not_adult}
end
_ ->
{:error, :age_missing}
end
end
# Rather use one with-statement like this:
check_access_refactored = fn details ->
with %{age: age} <- details,
true <- adult?.(age) do
:ok
else
# Gets executed if any of the matches above fails.
%{} -> {:error, :age_missing}
false -> {:error, :not_adult}
end
end
```
To match inside the `else`-block isn't great though. What if two matches can return `false`? How would you know which match failed?
It is good practice to move steps step into small helper functions. This allows you to return a specific error message depending on which step failed and it makes the `with`-statement more readable. It also makes it easy to see the "happy path" of your function and all its error cases.
```elixir
defmodule RunElixir.Permissions do
def check_access(details) do
with {:ok, age} <- get_age(details),
:ok <- check_adult(age) do
:ok
end
end
# Moving each step into a small helper function gives you the flexibility
# to decide which error to return inside the function.
defp get_age(details) do
case details do
%{age: age} when is_integer(age) -> {:ok, age}
%{age: _age} -> {:error, :age_invalid}
_ -> {:error, :age_missing}
end
end
defp check_adult(age) do
if age >= 18, do: :ok, else: {:error, :not_adult}
end
end
RunElixir.Permissions.check_access(%{age: 32}) # => :ok
RunElixir.Permissions.check_access(%{age: nil}) # => {:error, :age_invalid}
RunElixir.Permissions.check_access(%{name: "Peter"}) # => {:error, :age_missing}
RunElixir.Permissions.check_access(%{age: 17}) # => {:error, :not_adult}
```