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 to true.
  • First clause: applies only when age is an integer and between 0 and 17. It returns {:ok, "Minor"}.
  • Second clause: applies only when age is 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 _age to signal it is intentionally ignored. It returns {:error, "Invalid age"}.
  • The three IO.inspect calls show:
    • 15 -> {:ok, "Minor"}
    • 25 -> {:ok, "Adult"}
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 the operator (with lists/ranges)
Operator Nuances in Guards
  • Use and, or, not in guards. The operators &&, ||, ! are not allowed in guard expressions.
  • Precedence: and/or have 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:
  • Checking a map has a key:

    • Instead of Map.has_key?(m, :age) (not allowed), match the map shape:
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:

  1. case: If a pattern matches but the guard fails, Elixir tries the next clause. If no clauses match, a CaseClauseError is raised.
  2. 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.
Summary and Next Steps

Today you learned how to:

  • Refine matches: Use when to attach type and range checks to function heads and control flow structures.
  • Stay "Guard-Safe": Only use allowed operations like is_integer/1 and and/or/not to avoid compilation errors.
  • Manage logic: Handle overlapping conditions using top-to-bottom ordering and defguard for reusability.
  • Differentiate context: Understand how failures vary between case (errors), receive (skips), and with (halts).

Ready to apply it? Head to the practice section and put guard clauses to work.

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal