4 min read
Stop Writing Statements, Start Writing Expressions
Why Everything in Elixir Returns a Value (and What That Changes)

If you’re coming from Ruby or JavaScript, you’re used to some code doing things and other code returning things. Statements versus expressions. Side effects versus values.

Elixir collapses that distinction entirely. Everything is an expression that returns a value—including things that look like control flow.

How Expressions Replace Statements

In most languages, if is a statement that executes conditionally. It doesn’t return anything, it just does something based on a condition.

Elixir works differently:

result = if user_active? do
  calculate_score(user)
else
  0
end

That if isn’t control flow. It’s an expression that evaluates to a value. You can bind it, pass it to functions, or use it anywhere you’d use any other expression.

Bruce explains it this way:

“Everything in Elixir is an expression. An expression in Elixir is a bit of code that returns a value. And values in Elixir are called terms.”
— Bruce Tate

This applies to everything: case statements, function bodies, error handling through pattern matching. If you can write it in Elixir, it returns a value you can reason about.

🎯 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.

Why This Eliminates State Management

In languages with statements, you declare variables before using them, then mutate them inside control structures:

if condition
  result = do_something()
else
  result = do_something_else()
end

use_result(result)

You’re managing state. The variable exists before the if, gets mutated inside it, then gets used after.

In Elixir, the if produces the value directly:

result = if condition, do: do_something(), else: do_something_else()

use_result(result)

No mutation. No separate declaration. The expression evaluates to the value you need.

When everything is an expression, you stop managing state between operations and start composing values through transformations.

How to Read Elixir Code

Here’s where expression-based thinking changes how you read code. Look at this Phoenix LiveView update:

def handle_event("save", %{"post" => params}, socket) do
  socket.assigns.post
  |> Posts.update(params)
  |> case do
    {:ok, post} ->
      {:noreply, assign(socket, :post, post)}
    {:error, changeset} ->
      {:noreply, assign(socket, :changeset, changeset)}
  end
end

Every line is an expression. The post gets updated. That result gets matched. Each match returns a value. The function returns that value.

You’re not reading a sequence of steps that modify state. You’re reading a flow of transformations where each expression produces the input for the next one.

This is why the pipe operator exists—it makes the transformation chain visible:

user_input
|> String.trim()
|> String.downcase()
|> validate_email()

Each function receives the previous expression’s value and returns a new one. No hidden mutations. No side effects. Just data flowing through transformations.

What to Practice Today

Here’s something concrete: when you write LiveView event handlers, let the expressions do the work.

Instead of managing variables through conditional logic:

def handle_event("toggle_status", _, socket) do
  current_status = socket.assigns.status

  new_status = if current_status == :active do
    :inactive
  else
    :active
  end

  new_socket = assign(socket, :status, new_status)
  {:noreply, new_socket}
end

Write it as a chain of expressions:

def handle_event("toggle_status", _, socket) do
  new_status = if socket.assigns.status == :active, do: :inactive, else: :active

  {:noreply, assign(socket, :status, new_status)}
end

The if is an expression. The assign is an expression. The tuple is an expression. Each produces exactly what the next one needs.

When you catch yourself declaring a variable just to use it once, stop. The expression already produces the value. Let it flow directly.

What Comes Next

Understanding that everything returns a value sets up the next concept: how Elixir handles what look like variables but aren’t actually variables at all.

Next week, we’ll look at bindings and immutability, not as constraints, but as the design principle that makes concurrent Elixir code predictable.

See you in the next chapter.

— Paulo Valim & Bruce Tate


Want to Learn Elixir the Way It's Actually Designed?

This post is part of our series following Bruce Tate's structured Elixir course, where concepts build on each other naturally, not as isolated facts.

Bruce Tate's avatar
Bruce Tate
System architecture expert and author of 10+ Elixir books.
Paulo Valim's avatar
Paulo Valim
Full-stack Elixir developer and educator teaching modern Elixir and AI-assisted development.