How do function returns work without a return statement in EK9?
← Getting Started · Ref: Q50
EK9 deliberately has NO return statement. Instead, you declare a named return variable with '<-' and the compiler ensures all code paths initialise it. This eliminates an entire class of bugs where code paths miss a return.
INITIALISED RETURNS
The simplest pattern provides a default value at declaration:
add() as pure -> a as Integer, b as Integer <- result as Integer: a + b
Here 'result' is initialised to 'a + b' and the function needs no body.
RETURNS WITH BODY
When you need conditional logic, declare a default then modify:
clamp() as pure -> number as Integer, min as Integer, max as Integer <- result as Integer: number if number < min result: min else if number > max result: max
UNINITIALISED RETURNS — THE '?' SUFFIX
The '?' on '<- result as Float?' does NOT mean Optional. It means the return variable is declared but NOT initialised at declaration. It WILL be assigned during the function body. This is essential for pure functions where you want to use ':=?' (guarded assignment) across different conditional branches:
ratingPercentage() as pure -> percentage as Float <- rtn as String: given percentage <- rating as String: String() when <= 0.5 rating:=? "Low" when <= 0.8 rating:=? "Medium" default rating:=? "High"
The ':=?' operator assigns ONLY if the target is unset. Each branch sets the value exactly once. If a branch accidentally executed twice, ':=?' would fail rather than silently overwrite. This guarantees single-assignment semantics in pure functions.
UNSET RETURN AS SIGNAL
Another powerful pattern uses an unset return to signal 'not found':
findIndex()
-> items as List of Integer, target as Integer
<- index as Integer: Integer()
low <- 0
high <- length items - 1
while low <= high and ~index?
mid <- (low + high) / 2
checkValue <- items.getOrDefault(mid, 0)
if checkValue == target
index: mid
else if checkValue < target
low: mid + 1
else
high: mid - 1
The return 'index' starts as unset (Integer() creates an unset Integer). The caller checks 'if result?' to see if a value was found. The '~index?' in the while condition means 'index is not set' — the loop exits when found.
No return statement means no early exits, no forgotten returns, and no unreachable code. The compiler verifies every path. See Q29 for unset variable semantics, Q51 for abstract function implementations that use ':=?' patterns, and Q54 for how purity connects to ':=?'. See Q93 for class methods which use the same return patterns. See Q256 for the Void type and implicit returns. See Q274 for why AI generates return statements. See Q289 for converting multiple returns to EK9. See Q316 for how EK9 detects discarded return values.
Example
defines module qa.functionreturns defines function add() as pure -> a as Integer b as Integer <- result as Integer: a + b clamp() as pure -> number as Integer min as Integer max as Integer <- result as Integer: number if number < min result: min else if number > max result: max rate() as pure -> score as Integer <- rating as String? premiumThreshold <- 80 standardThreshold <- 60 if score >= premiumThreshold rating:=? "Excellent" else if score >= standardThreshold rating:=? "Good" else rating:=? "Needs work" findIndex() -> items as List of Integer target as Integer <- index as Integer: Integer() low <- 0 high <- length items - 1 while low <= high and ~index? mid <- (low + high) / 2 checkValue <- items.getOrDefault(mid, 0) if checkValue == target index: mid else if checkValue < target low: mid + 1 else high: mid - 1 defines program ReturnDemo() stdout <- Stdout() // Initialised return — no body needed stdout.println(`add(3, 4): ${add(3, 4)}`) // Return with body — default modified conditionally stdout.println(`clamp(15, 0, 10): ${clamp(15, 0, 10)}`) // Uninitialised return with :=? guarded assignment stdout.println(`rate(85): ${rate(85)}`) stdout.println(`rate(70): ${rate(70)}`) stdout.println(`rate(40): ${rate(40)}`) // Unset return as signal — findIndex numbers <- [1, 3, 5, 7, 9, 11] found <- findIndex(numbers, 7) if found? stdout.println(`Found 7 at index: ${found}`) notFound <- findIndex(numbers, 4) if ~notFound? stdout.println("4 not found in list")
Common mistakes
E08050 — If the return variable is uninitialised (? suffix) the compiler requires all paths to assign it. Missing the else branch means 'rating' could remain uninitialised. See ek9 -h E11064 for details.
Incorrect:
<- rating as String? if score >= 80 rating:=? "Excellent" else if score >= 60 rating:=? "Good"
Correct:
<- rating as String? premiumThreshold <- 80 standardThreshold <- 60 if score >= premiumThreshold rating:=? "Excellent" else if score >= standardThreshold rating:=? "Good" else rating:=? "Needs work"
E50030 — A String literal cannot initialise an Integer return variable. EK9 is strongly typed and requires compatible types in assignments. See ek9 -h E50030 for details.
Incorrect:
<- result as Integer: "sum"
Correct:
<- result as Integer: a + b
Other ways to ask this
- Why does EK9 not have a return statement?
- What does the question mark mean on a function return declaration?
- How does the guarded assignment operator work with function returns?
Coming from another language?
Java: return statement required, easy to forget on some paths, compiler warns but does not always catch all cases, no guarded assignment concept. Python: return statement, implicit None return if omitted, no compile-time path analysis. JavaScript: return statement, undefined if omitted, no compile-time checking. Rust: last expression is implicit return OR explicit return keyword, no named return variable, no guarded assignment. Go: named returns exist but return statement still required, bare return uses named values but is considered bad practice. C#: return statement required, no guarded assignment. Kotlin: return statement or last expression, no named return variable, no guarded assignment. Swift: return statement or implicit single-expression return, no named return, no guarded assignment. EK9: NO return statement at all, named return variable with '<-', compiler verifies all paths initialise, '?' suffix for uninitialised declaration, ':=?' guarded assignment ensures single assignment in pure functions, unset return as signal pattern eliminates sentinel values.
Keywords: question, named, isset, path, signal, first, variable, start, guarded, safe, beginner, null-safe, pure, uninitialised, function, mark, immutable, statement, intro, side-effect, assignment, return