How do I chain multiple guard expressions for safe access?

← Data Flow Safety · Ref: Q636

When you need to access multiple values that might be unset, chain guard expressions by nesting if-guards or using sequential guards with explicit isSet checks.

NESTED GUARDS

Nest guard expressions to ensure all values are set:

  if name <- findName(id)
    if score <- findScore(id)
      result: formatResult(name, score)

Each level guarantees its variable is set before the inner block executes.

SEQUENTIAL GUARDS

Use separate guard expressions in sequence when values are independent:

  if name <- findName(id)
    stdout.println(name)
  if score <- findScore(id)
    stdout.println(score)

Each guard independently checks its value.

GUARD WITH ELSE

Provide fallback behavior when a guard fails:

  if name <- findName(id)
    greet(name)
  else
    greetStranger()

WHY CHAIN GUARDS

Chaining guards ensures that every value used in a computation is guaranteed set. The compiler verifies each guard protects its variable scope. Without the guard, accessing the value would be unsafe (E08030).

See Q634 for guard basics. See Q163 for Optional unwrapping. See Q47 for Optional type patterns.

Example

defines module qa.dataflow.chainedguard

  defines constant

    USER_ALICE <- 1

    USER_CHARLIE <- 3

    UNKNOWN_USER <- 999

  defines function

    findFirstName()
      -> userId as Integer
      <- rtn as String: String()

      if userId == USER_ALICE
        rtn: "Alice"
      else if userId == USER_CHARLIE
        rtn: "Charlie"

    findLastName()
      -> userId as Integer
      <- rtn as String: String()

      if userId == USER_ALICE
        rtn: "Smith"
      else if userId == USER_CHARLIE
        rtn: "Brown"

    findAge()
      -> userId as Integer
      <- rtn as Integer: Integer()

      if userId == USER_ALICE
        rtn: 30
      else if userId == USER_CHARLIE
        rtn: 25

    findDepartment()
      -> userId as Integer
      <- rtn as String: String()

      if userId == USER_ALICE
        rtn: "Engineering"

  defines program

    ChainedGuardDemo()
      stdout <- Stdout()

      //Nested guards: all must be set to build full profile
      if first <- findFirstName(USER_ALICE)
        if last <- findLastName(USER_ALICE)
          if age <- findAge(USER_ALICE)
            stdout.println(`${first} ${last}, age ${age}`)

      //Sequential independent guards
      if nameA <- findFirstName(USER_ALICE)
        stdout.println(`Found: ${nameA}`)

      if nameB <- findFirstName(UNKNOWN_USER)
        stdout.println(`Found: ${nameB}`)
      else
        stdout.println("User not found")

      //Guard with additional nested guard
      if dept <- findDepartment(USER_ALICE)
        if userName <- findFirstName(USER_ALICE)
          stdout.println(`${userName} works in ${dept}`)

      //Explicit isSet check pattern
      ageResult <- findAge(USER_CHARLIE)
      if ageResult?
        stdout.println(`Age found: ${ageResult}`)

Common mistakes

E50001 — Using first before its guard declaration is a forward reference error. The guard must be evaluated before using the variable. See ek9 -h E50001 for details.

Incorrect:

stdout.println(`${first} ${last}`)
      if first <- findFirstName(USER_ALICE)

Correct:

if first <- findFirstName(USER_ALICE)
        if last <- findLastName(USER_ALICE)
Other ways to ask this
  • Can I nest guard expressions in EK9?
  • How do I safely access multiple potentially-unset values?
  • What is the pattern for multiple optional lookups?

Coming from another language?

Java: nested if-present checks on Optional, pyramid of doom. Kotlin: safe call chaining ?. operator, scope functions let/run. Swift: if let chaining, optional binding. Rust: pattern matching with nested Some/None. Go: repeated nil checks. EK9: nested guard expressions 'if var <- expr()' with compiler-enforced scoping.

Keywords: safe, guard, access, E08030, chain, optional, E08040, absent, isset, nested, null-safe, scope, initialize, safety, data-flow, multiple