How do Result guard patterns and ternary access work?

← Getting Started · Ref: Q86

In EK9, Result guard patterns let you safely access ok and error values through compiler-enforced checks. The key insight is that isOk() and isError() are INDEPENDENT guards: isOk() unlocks .ok(), isError() unlocks .error(), and neither unlocks the other.

DUAL GUARD PATTERN

Check both sides of a Result independently using if/else if:

  if r.isOk()
    okVal <- r.ok()
  else if r.isError()
    errVal <- r.error()

The compiler tracks which guard is active. Inside the isOk() branch, only .ok() is permitted. Inside the isError() branch, only .error() is permitted. Using the wrong accessor is a compile error.

? OPERATOR GUARDS OK ONLY

The ? operator is shorthand for isOk(), so it only unlocks .ok():

  if r?
    okVal <- r.ok()

You cannot call .error() inside a ? guard block. For error access, you must use isError() explicitly.

TERNARY GUARD PATTERNS

Get a value or a default in a single expression:

  okVal <- r.isOk() <- r.ok() else String()
  errVal <- r.isError() <- r.error() else Integer()

The ternary reads as: declare okVal, check isOk(), if true assign r.ok(), otherwise assign the default. This is concise and safe. The compiler verifies the guard matches the accessor.

AND LOGIC WITH MULTIPLE RESULTS

Combine guards across multiple Results with 'and':

  if r1? and r2.isError()
    okVal <- r1.ok()
    errVal <- r2.error()

Each guard independently unlocks its own Result. The block only runs if ALL conditions pass.

NO REASSIGNMENT IN SAFE BLOCKS

Once a Result is guarded, the compiler prevents reassignment inside the safe block:

  if r?
    r: otherResult    COMPILER ERROR: NO_REASSIGNMENT_WITHIN_SAFE_ACCESS

This prevents invalidating the guard check by swapping the Result for one that might not be ok.

See Q48 for Result basics and creation patterns. See Q87 for Result operations (merge, copy, callbacks). See Q74 for guard patterns in if statements. See Q139 for when to use Result vs try/catch exception handling. See Q259 for how EK9's four-state Result differs from other languages.

Example

defines module qa.result.guards

  defines function

    <?-
      Returns a Result with ok value set.
    -?>
    getOkResult()
      <- rtn <- Result("Success", Integer())

    <?-
      Returns a Result with error value set.
    -?>
    getErrorResult()
      <- rtn <- Result(String(), 42)

    <?-
      Returns a Result with both ok and error set.
    -?>
    getBothResult()
      <- rtn <- Result("Partial", -1)

  defines program

    ResultGuardDemo()
      stdout <- Stdout()

      // === DUAL GUARD PATTERN ===

      r1 <- getOkResult()
      if r1.isOk()
        stdout.println(`Dual ok: ${r1.ok()}`)
      else if r1.isError()
        stdout.println(`Dual error: ${r1.error()}`)

      r2 <- getErrorResult()
      if r2.isOk()
        stdout.println(`Should not print`)
      else if r2.isError()
        stdout.println(`Dual error side: ${r2.error()}`)

      // === ? OPERATOR GUARDS OK ONLY ===

      r3 <- getOkResult()
      if r3?
        stdout.println(`? guard ok: ${r3.ok()}`)

      r4 <- getErrorResult()
      if ~r4?
        stdout.println("Error result: ? is false as expected")

      // === isOk GUARDS ok, isError GUARDS error — INDEPENDENT ===

      r5 <- getBothResult()
      if r5.isOk()
        stdout.println(`Both has ok: ${r5.ok()}`)
      if r5.isError()
        stdout.println(`Both has error: ${r5.error()}`)

      // === TERNARY GUARD: ok with default ===

      r6 <- getOkResult()
      okVal <- r6.isOk() <- r6.ok() else String()
      stdout.println(`Ternary ok: ${okVal}`)

      // === TERNARY GUARD: error with default ===

      r7 <- getErrorResult()
      errVal <- r7.isError() <- r7.error() else Integer()
      stdout.println(`Ternary error: ${errVal}`)

      // Ternary on empty Result falls back to default
      r8 <- Result() of (String, Integer)
      fallback <- r8.isOk() <- r8.ok() else "default"
      stdout.println(`Ternary fallback: ${fallback}`)

      // === AND LOGIC WITH MULTIPLE RESULTS ===

      rOk <- getOkResult()
      rErr <- getErrorResult()
      if rOk? and rErr.isError()
        stdout.println(`Combined: ok=${rOk.ok()}, error=${rErr.error()}`)

      // Both-value Result: can guard both independently
      rBoth <- getBothResult()
      if rBoth.isOk() and rBoth.isError()
        stdout.println(`Both sides: ${rBoth.ok()} with ${rBoth.error()}`)

      // === SAFE BLOCK SCOPE ===
      // Inside a guard block, the guarded Result cannot be reassigned.
      // This prevents invalidating the safety guarantee.
      // Attempting: r: otherResult inside if r? would produce
      // COMPILER ERROR: NO_REASSIGNMENT_WITHIN_SAFE_ACCESS

      r9 <- getOkResult()
      if r9?
        safeOk <- r9.ok()
        stdout.println(`Safe access: ${safeOk}`)
        // r9 is protected here — no reassignment allowed

Common mistakes

E08030 — Calling .ok() without an isOk() or ? guard triggers E08030 — has not been checked before access. The compiler enforces that isOk() must be checked before .ok(), and isError() before .error(). See ek9 -h E08030 for details.

Incorrect:

stdout.println(`? guard ok: ${r3.ok()}`)

Correct:

if r3?
        stdout.println(`? guard ok: ${r3.ok()}`)

E08040 — Reassigning a Result variable inside a guard block triggers E08040 — reassignment/mutation within safe access scope is not allowed. The guard verified r9 was ok; reassigning invalidates that check. See ek9 -h E08040 for details.

Incorrect:

if r9?
        r9 := getErrorResult()
        safeOk <- r9.ok()

Correct:

if r9?
        safeOk <- r9.ok()
Other ways to ask this
  • How does the dual guard pattern work with Result isOk and isError?
  • How do I use ternary Result access to get ok or error values with defaults?
  • What are the safe access patterns for Result values in EK9?

Coming from another language?

Java: try-catch blocks handle errors but have no compile-time enforcement that catch blocks are present for unchecked exceptions, no safe accessor pattern, can forget to handle the error path entirely. Rust: match on Result<T,E> with Ok(v)/Err(e) arms provides exhaustive checking, but .unwrap() is an escape hatch that panics at runtime, no independent ok+error access since Result is strictly either/or. Go: comma-ok idiom 'val, err := doThing(); if err != nil' is convention not enforcement, error can be silently ignored with _, no compiler-enforced safe access. EK9: isOk() and isError() are independent compiler-enforced guards, no escape hatches, ternary guard gives concise safe access with defaults, no reassignment rule prevents guard invalidation.

Keywords: isOk, guard, null-safe, ternary, access, first, isError, intro, pattern, safe, result, isset, reassignment, ok, beginner, dual, error, start