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 with no value at all ('as Type?') and used it before assigning one:

  count as Integer?      declared, no value yet
  total <- count + 1     // ERROR E08020: count might be used before being initialised
  stdout.println($total)

Fix: give it a value before the use (or declare it with one):

  count as Integer?
  count: 0
  total <- count + 1     now safe

Note the difference: count <- Integer() IS initialised. It holds a present-but-unset Integer, can be used freely, and count + 1 is then simply unset.

E08030 - Not Checked Before Access:
You called an accessor on an Optional, Result or Iterator without checking it first:

  lookup()
    <- rtn as Optional of String: Optional("found")
  maybe <- lookup()
  stdout.println(maybe.get())     // ERROR E08030: maybe has not been checked before access

Fix: check it, or use a guard:

  if maybe?
    stdout.println(maybe.get())   safe

An ordinary value such as Integer(someString) needs no check to be used; it may simply be unset.

E08050 - Return Not Always Initialised:
Your function declares its return variable with no value and some path never assigns it:

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

Fix: assign it on ALL paths, or give it an initial value (<- rtn as Float: Float()).

GUARDS

The preferred way to handle potentially unset values is with guards. 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.

ASSIGN-IF-UNSET (:=?)

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

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

This is safe because :=? checks first.

COALESCING OPERATORS

The coalescing operator ?: (Elvis) provides a default when a value is unset:

  safe <- maybeUnset ?: "fallback"

The ?? operator is NOT a default for an unset value: it only replaces an ABSENT one, so an unset value such as String() passes straight through and the result is still unset (see Q1059).

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: 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")

      // Assign-if-unset 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 (Elvis) ?: (?? would pass the unset value through)
      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, guards, :=? assign-if-unset, no escape hatches.

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