How do guard variables work in if statements?

← Control Flow · Ref: Q74

Guard variables let you combine variable creation with an isSet check in a single if statement. The guard ensures the if body only executes when the value is meaningful.

BASIC GUARD PATTERN

Use the declaration operator (<-) to create a guard variable:

  if value <- getResult()
    stdout.println(value)

The variable 'value' is created and checked. If getResult() returns an unset value, the if body is skipped entirely. No exception, no crash.

GUARD WITH ADDITIONAL CONDITION

Use 'with' or 'then' to add a second condition after the guard:

  if value <- getResult() with value > 10
    stdout.println(value)

Both must be true: the guard must be SET and the extra condition must pass. Short-circuit applies: if the guard is unset, the condition is never evaluated.

IF/ELSE WITH GUARD

The else branch executes when the guard is unset:

  if value <- getResult()
    process(value)
  else
    handleMissing()

THREE GUARD OPERATORS

EK9 supports three operators in if guards, each with different semantics:

  1. Declaration (<-): Creates NEW variable, checks isSet of result
     if value <- getResult()
  2. Assignment (:=): Assigns to EXISTING variable, NO isSet check (blind)
     if existing := getResult() with existing > 0
  3. Guarded assignment (?=): Assigns to EXISTING variable, checks isSet of RHS
     if existing ?= getResult()

The declaration guard (<-) is by far the most common and recommended form.

SCOPING

Guard variables created with <- are scoped to the if/else block. They do not leak into the surrounding scope, preventing accidental reuse of one-time values.

WHAT 'SET' MEANS

The guard calls the type's isSet (?) operator. Each type defines what 'set' means: Integer is set when it has a numeric value, String when it has text content, Optional when it contains a value, collections when they have been properly initialized.

See Q29 for unset variables. See Q61 for basic if/else without guards. See Q75 for guards in switch. See Q76 for guards in for loops. See Q77 for guards in while loops. See Q78 for guards in try blocks. See Q166 for consistent safe pattern. See Q269 for input validation with guards. See Q272 for defense in depth with guards. See Q285 for multiple precondition validation with guards.

Example

defines module qa.flow.guard.ifstatement

  defines record

    Config
      host <- String()
      port <- Integer()

      default operator ?

  defines function

    getConfig()
      <- rtn <- Config()

    getActiveConfig()
      <- rtn <- Config()
      rtn.host: "localhost"
      rtn.port: 8080

  defines program

    GuardIfDemo()
      stdout <- Stdout()

      // === BASIC GUARD: if value <- expr() ===

      if config <- getActiveConfig()
        stdout.println("Got config: " + config.host)
      else
        stdout.println("No config available")

      // === GUARD WITH ADDITIONAL CONDITION ===

      if config <- getActiveConfig() with config.port > 0
        stdout.println("Config port: " + $config.port)

      // === GUARD WITH UNSET VALUE ===

      if config <- getConfig()
        stdout.println("This should not print")
      else
        stdout.println("Config was unset, handled safely")

      // === ASSIGNMENT GUARD (no isSet check) ===

      existing <- Config()
      if existing := getActiveConfig() with existing.port > 0
        stdout.println("Assigned: " + existing.host)

      // === WHEN KEYWORD (alternative to if) ===

      when config <- getActiveConfig()
        stdout.println("When guard works too: " + config.host)

Common mistakes

E01072 — EK9 has no return statement. Guard variables in if statements combine declaration and isSet checking in one expression, replacing the early-return-if-null pattern. See ek9 -h E01072 for details.

Incorrect:

config <- getActiveConfig()
      if not config?
        return
      stdout.println("Got config: " + config.host)

Correct:

if config <- getActiveConfig()
        stdout.println("Got config: " + config.host)
      else
        stdout.println("No config available")
Other ways to ask this
  • How do I combine variable declaration with an if condition?
  • What is the if guard pattern in EK9?
  • How does EK9 handle null-safe if checks?
  • How do I use the declaration operator in an if statement?

Coming from another language?

Java: No guard syntax. Must write: 'var v = getValue(); if (v != null) { use(v); }' as two separate steps. Variable leaks into outer scope. Optional requires: 'getValue().ifPresent(v -> use(v))' which forces a lambda. Python: No guard syntax. Walrus operator (:=) in 3.8+ comes close: 'if (v := getValue()) is not None:' but only handles None, not general isSet semantics. Rust: 'if let Some(v) = get_value()' for Option destructuring. Close to EK9 guards but limited to pattern matching. Go: 'if v := getValue(); v != nil' allows init statement in if but uses nil checks, no isSet concept. Kotlin: 'val v = getValue(); if (v != null)' with smart cast. No single-expression guard. Swift: 'if let v = getValue()' for optional binding, closest to EK9 but limited to Optional type. EK9: 'if value <- getResult()' combines declaration, assignment, and isSet check in one expression. Works with any type that has the ? operator, not just Optional. Three operator variants for different semantics.

Keywords: declaration, branch, operator, null-safe, isset, control, condition, unset, flow, guard, null, safe, if, scope, check