Why does the compiler say my variable is unset?

← Debugging and Troubleshooting · Ref: Q251

The EK9 compiler uses flow analysis to track whether variables have been initialised. If you use a variable before giving it a value, or access a value without checking if it is set, the compiler reports an error.

COMMON ERRORS AND FIXES

E08020 - Used Before Initialised:
You declared a variable but used it before assigning a value:

  count <- Integer()     unset
  total <- count + 1     ERROR: count used before initialised

Fix: assign a value first, or use a guard:

  count <- Integer()
  count :=? 0
  total <- count + 1     now safe

E08030 - Not Checked Before Access:
You accessed a value that might be unset without a ? guard:

  parsed <- Integer(someString)
  stdout.println($parsed)     ERROR: parsed not checked

Fix: wrap in a guard:

  parsed <- Integer(someString)
  if parsed?
    stdout.println($parsed)   safe

E08050 - Return Not Always Initialised:
Your function has code paths where the return value is never set:

  calculateArea()
    -> shape as String
    <- rtn as Float: Float()
    if shape == "circle"
      rtn: 3.14
    // ERROR: rtn not set when shape is not circle

Fix: ensure ALL paths set the return value.

GUARD EXPRESSIONS

The preferred way to handle potentially unset values is with guard expressions. These combine assignment with a ? check:

  if value <- computeSomething()
    stdout.println(`Got: ${value}`)

The block only executes if value is set. This works identically in if, switch, for, while, and try.

GUARDED ASSIGNMENT (:=?)

The :=? operator only assigns if the target is currently unset:

  name <- String()
  name :=? "default"

This is safe because :=? checks first.

COALESCING OPERATORS

The ?? operator provides a default when a value is unset:

  safe <- maybeUnset ?? "fallback"

The ?: operator evaluates a function when unset:

  safe <- maybeUnset ?: getDefault

OPTIONAL AND RESULT

Optional requires a ? check before calling .get(). Result requires isOk() before .ok() and isError() before .error(). The compiler enforces these guards at compile time with no escape hatches.

DIVISION BY ZERO

Dividing by zero in EK9 does NOT throw an exception. It returns an unset value:

  result <- 10 / 0
  // result is unset, not an error
  if result?
    stdout.println($result)
  else
    stdout.println("Division produced unset")

See Q29 for tri-state semantics. See Q47 for Optional safe access. See Q48 for Result safe access. See Q75 for guard patterns in switch. See Q243 for coalescing operators. See Q632 for definition ordering. See Q633 for branch initialization. See Q634 for guard-based safe access.

Example

defines module qa.debugging.variableunset

  defines function

    safeDivide() as pure
      ->
        a as Integer
        b as Integer
      <- rtn <- Integer()

      if b <> 0
        rtn: a / b

    findItem() as pure
      -> items as List of String
      <- rtn <- Optional() of String

      for item in items
        if item == "target"
          rtn: Optional(item)

  defines program

    VariableUnsetDemo()
      stdout <- Stdout()

      // Guard expression: only executes if set
      if result <- safeDivide(10, 3)
        stdout.println(`Division result: ${result}`)

      // Division by zero returns unset
      if zeroResult <- safeDivide(10, 0)
        stdout.println("Should not print")
      else
        stdout.println("Division by zero produced unset")

      // Guarded assignment with :=?
      name <- String()
      stdout.println(`Before guard: set=${name?}`)
      name :=? "default"
      stdout.println(`After guard: ${name}`)
      name :=? "other"
      stdout.println(`After second guard: ${name}`)

      // Coalescing operator ??
      unsetVal <- String()
      safe <- unsetVal ?? "fallback"
      stdout.println(`Coalesced: ${safe}`)

      // Optional with guard
      items <- ["apple", "target", "cherry"]
      if found <- findItem(items)
        foundItem <- found.get()
        stdout.println(`Found: ${foundItem}`)

      // Integer parsing with guard
      parsed <- Integer("42")
      if parsed?
        stdout.println(`Parsed: ${parsed}`)

      badParsed <- Integer("xyz")
      if badParsed?
        stdout.println("Should not print")
      else
        stdout.println("Invalid input: integer is unset")

Common mistakes

E50060 — Stdout does not have a display() method. The correct method is println(). Calling a non-existent method triggers E50060 — method not resolved. See ek9 -h E50060 for details.

Incorrect:

stdout.display(`Before guard: set=${name?}`)

Correct:

stdout.println(`Before guard: set=${name?}`)
Other ways to ask this
  • How do I fix unset variable errors in EK9?
  • What does 'used before initialised' mean in EK9?
  • How do I handle unset values in EK9?

Coming from another language?

Java: null by default, NullPointerException at runtime, no compile-time null tracking (unless using annotations). Python: NameError for undefined, None checks are convention not enforced. Rust: no null, Option/Result with compiler-enforced pattern matching. Go: zero values by default (0, "", nil), nil pointer panics at runtime. Kotlin: nullable types with compile-time null safety, but !! escape hatch exists. Swift: Optional with compile-time enforcement, ! force-unwrap crashes. EK9: tri-state model, compile-time flow analysis tracks initialisation, guard expressions, :=? conditional assignment, no escape hatches.

Keywords: guard, E08020, used, error-message, null-safe, unset, before, E08030, initialize, variable, analysis, debug, initialise, safe, compile, check, isset, troubleshoot, E08050, set, flow