Using Guard Clauses
Guard Clauses: Adding Conditions to Your Matches
Welcome back. In the previous lesson, you used multiple function clauses to match different input shapes right in the function head. As a reminder, we also briefly used a guard to prevent dividing by zero. In this lesson, you will focus on guard clauses themselves — how to attach conditions to a clause so it runs only when certain checks (type, range, etc.) are true.
Guard Clauses in Action
Explanation:
- The module defines three clauses of the same function,
validate_age/1. Elixir tries them from top to bottom and picks the first one whose pattern matches and whose guard evaluates totrue. - First clause: applies only when
ageis an integer and between 0 and 17. It returns{:ok, "Minor"}. - Second clause: applies only when
ageis an integer and at least 18. It returns{:ok, "Adult"}. - Third clause: no guard, catches everything else — negative numbers, non-integers, etc. The parameter is named
_ageto signal it is intentionally ignored. It returns{:error, "Invalid age"}. - The three
IO.inspectcalls show:15->{:ok, "Minor"}25->{:ok, "Adult"}-5->{:error, "Invalid age"}(it fails the first guard due toage >= 0and fails the second guard due toage >= 18)
- Why guards? They let you keep your routing logic clean and declarative. You match the input shape in the head, then refine with boolean checks like
is_integer(age), comparisons, and logicaland. Note: Guards accept a limited, “guard-safe” set of operations — perfect for validation like this.
What’s Allowed in Guards
- Type checks:
is_atom/1,is_binary/1,is_bitstring/1,is_boolean/1,is_float/1,is_function/1,2,is_integer/1,is_list/1,is_map/1,is_nil/1,is_number/1,is_pid/1,is_port/1,is_reference/1,is_tuple/1 - Comparisons:
==,!=,===,!==,<,<=,>,>=, and theinoperator (with lists/ranges) - Arithmetic and numeric:
+,-,*,div/2,rem/2,abs/1 - Boolean operators:
and,or,not(see operator nuances below) - Size/access helpers:
byte_size/1,bit_size/1,tuple_size/1,map_size/1,elem/2,map_size/1
Not allowed:
- User-defined functions or most library calls (e.g.,
Enum.*,Map.*). For example,Map.has_key?/2,Enum.count/1,length/1,hd/1,tl/1, and the/operator (floating-point division) are not allowed in guards. - Operators
&&,||,!are not allowed in guards; useand,or,notinstead.
Important: Only use functions and operators that are explicitly documented as guard-safe in the Elixir documentation. Using non-guard-safe functions in guards will cause compilation errors. Always avoid non-guard-safe functions in guard expressions.
Operator Nuances in Guards
- Use
and,or,notin guards. The operators&&,||,!are not allowed in guard expressions. - Precedence:
and/orhave lower precedence than comparisons and arithmetic. Use parentheses in complex guards for readability.
Example:
Overlapping Guards and Ordering
Elixir evaluates clauses from top to bottom. If the pattern matches but the guard fails, it proceeds to the next clause.
When Something Isn’t Guard-safe
-
Checking list emptiness:
- Instead of
Enum.empty?(list)(not allowed), pattern-match the shape:
- Instead of
-
Checking a map has a key:
- Instead of
Map.has_key?(m, :age)(not allowed), match the map shape:
- Instead of
Reusable Guards
Compose domain checks once and reuse them across clauses.
You can also use guard macros in other guard-capable constructs (see below).
Guards Beyond Function Heads
Guards work in case, receive, and with expressions.
-
In
case: -
In
receive: -
In
with(guards on patterns in generators/clauses):
Differences: How They Handle Guard Failure
While the syntax for guards is the same across these constructs, how they behave when a guard fails differs:
case: If a pattern matches but the guard fails, Elixir tries the next clause. If no clauses match, aCaseClauseErroris raised.receive: If a guard fails, the message is not consumed. It remains in the process mailbox, and Elixir looks for the next message that might match.with: If a guard on a pattern match fails (e.g.,a when a >= 18 <- age), the execution stops immediately. The result of the entirewithblock becomes the value that failed the match (or it proceeds to theelseblock if one is defined).
Summary and Next Steps
Today you learned how to:
- Refine matches: Use
whento attach type and range checks to function heads and control flow structures. - Stay "Guard-Safe": Only use allowed operations like
is_integer/1andand/or/notto avoid compilation errors. - Manage logic: Handle overlapping conditions using top-to-bottom ordering and
defguardfor reusability. - Differentiate context: Understand how failures vary between
case(errors),receive(skips), andwith(halts).
Ready to apply it? Head to the practice section and put guard clauses to work.
