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.