elixir, control flow

Getting Started with Elixir - Control Flow

Control flow expressions are not used as often in Elixir as with more imperative languages mainly because controlling execution flow can be handled with a mix of pattern-matching, multi-clause functions and guard clauses.  

And though using control flow expressions may be easier to understand at first, as the complexity of a program increases, nested control structures can start to creep in.   Then we can simplify our code using the more functional patterns mentioned earlier.

There will definitely be times when you need to rely on Control Flow Expressions so it's worth understanding how they work.

Here's what we'll cover: if / unless; cond; case and with.

Elixir Series


  • Elixir 1.10.0

if and unless

If you're familiar with Ruby, if and unless are used in the same way in Elixir.


if 5 > 1 do
IO.puts "5 is greater than 1"
IO.puts "5 is less than 1"

5 is greater than 1 so the expression is true.


unless will only execute the block when the expression results in nil or false:

unless 5 > 1 do
IO.puts "5 is less than 1"
IO.puts "5 is greater than 1"

Here the expression 5 > 1 is neither nil or false so the else clause is executed.

NOTE:  it's recommended to avoid using else with unless. Prefer if / else.


if and unless can also be used with the inline notation:

iex> if 5 > 1, do: true, else: false

iex> unless 5 > 1, do: false, else: true

The else clause is optional. These examples will return nil if the main clause is falsy.


The cond structure is known as a multi-way if statement where the code associated with the first truthy condition is executed. 

This can be used as a replacement for if / else if statements.

Here's an example:

iex> x = 1

cond do
x > 1 -> "x is greater than 1"
x == 1 -> "x is equal to 1"
true -> "x is less than 1"

"x is equal to 1"

In this example, 1 is equal to 1 so the output was expected.

The last condition true serves as a default and will run when no other conditions are truthy.


case accepts an expression and works with the return value of the expression.  

It will pattern-match against the result from top to bottom running through the given patterns. Once a match is found, the code associated with it will run.

Let's start with a simple example:

case 3 do
1 -> "one"
2 -> "two"
3 -> "three"
_ -> "did not find a match!"


Case Expression Result

Because case is an expression itself, it's designed to return a value.

File.read/1 returns {:ok, binary}, where binary is a binary data object that contains the contents of path, or {:error, reason} if an error occurs.

Here's an example:

result = case File.read("data_file.txt") do
{:error, reason} -> "The file could not be read",
{:ok, data} -> "#{length(data)} bytes were read from the file}

We can now use the result of the case statement in our program.  Another contrived example, but it demonstrates that case is an expression.


with was introduced in Elixir 1.2 and can be used when a series of expressions need to be successful.  

with can also be used to replace nested case instances or even a group of multi-clause functions.

Here are some important flow concepts to understand when using with:

  • It accepts one or more expressions, a do block and an optional else clause.
  • It will pattern match on the return value of each expression and will only run the code in the do block when every pattern matches.
  • If one of the expressions do not have a match, it will return that expression's value.
  • If there is an else clause, this value will be return instead.

Let's use a real world example shared on twitter by @devoncestes simplified for readability:

def create_subscription(email, other_args) do
with {:ok, user} <- lookup_user(email),
{:ok, customer} <- create_customer(user),
{:ok, subscription} <- create_subscription(customer),
:ok <- update_user(user) do
{:ok, :subscription_created}

Let's walk through this function remembering that each expression must have a match before the code in the do block will run:

  • a user must be found
  • a customer must be created
  • a subscription must be created
  • the user must be updated successfully

If every expression has a match the function return value will be: {:ok, :subscription_created} 

Notice that no else clause was given.  This means that if any given expression doesn't have a successful match, its return value will be returned.

Wrapping Up

We've discussed the main control flow expressions in Elixir and used some examples to explore how they might be used.

Though it can be better to use a more functional programming style with multi-clause functions et al, there will inevitably be situations where using if / unless, cond, case or with makes more sense.

Elixir Series


I hope you're finding this series helpful.  If you find any issues or have any feedback feel free to hit me up on twitter: @tmartin8080 || @phxroad  or subscribe to the mailing list to receive occasional updates.

Author image

About Troy Martin

Ruby, Elixir and Javascript Software Developer.