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