Guards in Elixir
Adding extra boolean conditions to pattern matches with when, and the restricted set of expressions guards allow.
2 min read
Pattern matching checks the shape of data, but sometimes you need to check something about a value too — "this is a number, but only if it's positive." That's what guards are for: a when clause attached to a function head or case branch.
Guards on function clauses
defmodule Number do
def describe(n) when n < 0, do: "negative"
def describe(0), do: "zero"
def describe(n) when n > 0, do: "positive"
end
Number.describe(-5)
# => "negative"
Number.describe(10)
# => "positive"Without the guard, describe(n) would match any number, and Elixir wouldn't be able to tell the negative and positive clauses apart. The when clause narrows a pattern match with an extra condition evaluated after the shape matches.
Guards inside case
check_temp = fn temp ->
case temp do
t when t < 0 -> "freezing"
t when t >= 0 and t < 20 -> "cold"
t when t >= 20 and t < 30 -> "comfortable"
_t -> "hot"
end
end
check_temp.(25)
# => "comfortable"This reads almost like cond, but it keeps the value bound to a name (t) that the guard can reference, which is exactly what you need when the branching depends on comparing the matched value itself.
Why guards can't call arbitrary functions
Guards only permit a restricted, whitelisted set of expressions: comparison operators, boolean operators, arithmetic, type-checks (is_integer/1, is_binary/1, is_list/1, is_atom/1, and similar), and a handful of other pure functions like length/1, hd/1, and map_size/1.
defmodule Validator do
def valid_age?(age) when is_integer(age) and age >= 0 and age <= 150 do
true
end
def valid_age?(_age), do: false
endThis restriction is deliberate, not a missing feature: guard expressions must be side-effect-free and guaranteed not to raise, because the runtime needs to safely try a guard and move on to the next clause if it fails or errors. A guard that could throw an exception, or block on I/O, would break that guarantee — so calling a regular user-defined function directly inside a guard isn't allowed.
Combining multiple conditions
defmodule Access do
def can_edit?(role, is_owner)
when role == :admin or is_owner == true do
true
end
def can_edit?(_role, _is_owner), do: false
end
Access.can_edit?(:viewer, true)
# => true
Access.can_edit?(:viewer, false)
# => falseand/or (strict boolean operators) and &&/|| (which accept any truthy/falsy value) both work in guards, but the strict versions are preferred there since guard values are expected to already be actual booleans.
When to reach for guards
Use a guard whenever a pattern match alone can't express the distinction you need — most often type checks (is_list(x)), range checks (age >= 18), or simple relational comparisons between bound variables. For anything more complex than that — calling out to another function, checking something in a database, or multi-step logic — do the check inside the function body instead, since guards are intentionally kept small and predictable.
Test what you just learned
4 quick questions. Get all of them right to unlock the next lesson.
You can take the quiz without an account — logging in just lets your result count toward your progress.