How do I structure functions without early return in EK9?
← Control Flow · Ref: Q146
EK9 has no return statement. Instead of using early returns to handle different cases, decompose complex logic into small focused functions that each do one thing.
THE PROBLEM WITH EARLY RETURNS
In other languages, functions grow complex through accumulated early returns: validate input (return if bad), check permissions (return if denied), handle edge cases (return if empty), then finally process. Each return is a hidden exit. Paths multiply. Cleanup can be skipped. Testing requires tracing every possible exit.
THE EK9 APPROACH: DECOMPOSITION
Instead of one large function with early returns, write several small functions. Each function does one job. The calling function composes them.
SEPARATE VALIDATION FROM PROCESSING
Write a validation function that returns a Boolean or an unset signal:
validateAge() as pure -> age as Integer <- valid as Boolean: age >= 0 and age <= 150
Then guard on the result:
if validateAge(age)
processAge(age)
SEPARATE DIFFERENT PROCESSING PATHS
Instead of if/else chains with early returns, use a categorisation function followed by a switch:
categorise() as pure -> score as Integer <- category as String: "average" if score >= 90 category: "excellent" else if score >= 70 category: "good" else if score < 40 category: "poor"
WHY DECOMPOSITION IS BETTER
Each function does one job. All paths through a function are visible at a glance. There are no hidden exits. Each function is independently testable. The calling code reads as a sequence of clear steps rather than a maze of guard clauses.
GUARDS FOR CONDITIONAL EXECUTION
Use guard variables to execute code only when a value is available:
if record <- findRecord(id) process(record)
The body only executes if findRecord returns a set value. This replaces the early-return-if-null pattern.
UNSET RETURN AS SIGNAL
Functions can return an unset value to signal that no result was found:
findByName()
->
name as String
items as List of String
<- found as String: String()
for item in items
if item == name
found: item
The caller uses a guard to check: if result <- findByName(name, items).
See Q50 for declared return variables. See Q51 for abstract function patterns. See Q56 for higher-order function composition. See Q74 for guard variables in if statements. See Q144 for why return was removed. See Q145 for replacing break and continue. See Q285 for multiple precondition patterns. See Q289 for before and after migration examples.
Example
defines module qa.flow.philosophy.decomposition defines function // Validation function: one job validateAge() as pure -> age as Integer <- valid <- false maxAge <- 150 valid: age >= 0 and age <= maxAge // Categorisation function: one job categorise() as pure -> score as Integer <- category as String: "average" excellentMin <- 90 goodMin <- 70 poorMax <- 40 if score >= excellentMin category: "excellent" else if score >= goodMin category: "good" else if score < poorMax category: "poor" // Description function: one job describeCategory() as pure -> category as String <- description as String: category + " performance" // Search function: returns unset if not found findItem() -> name as String items as List of String <- found as String: String() for item in items if item == name found: item defines program DecompositionDemo() stdout <- Stdout() // === VALIDATION THEN PROCESSING === ages <- [25, -5, 200, 42, 0, 150] for age in ages if validateAge(age) stdout.println(`Valid age: ${age}`) else stdout.println(`Invalid age: ${age}`) // === CATEGORISE THEN DESCRIBE === scores <- [95, 72, 55, 38, 85] for score in scores category <- categorise(score) description <- describeCategory(category) stdout.println(`Score ${score}: ${description}`) // === GUARD ON SEARCH RESULT === fruits <- ["apple", "banana", "cherry"] if result <- findItem("banana", fruits) stdout.println(`Found: ${result}`) if result <- findItem("mango", fruits) stdout.println(`Found: ${result}`) else stdout.println("Mango not found")
Common mistakes
E01072 — EK9 has no return statement. Functions use declared return variables. Decompose complex logic into small focused functions that each assign to their return variable. See ek9 -h E01072 for details.
Incorrect:
validateAge() as pure -> age as Integer if age < 0 or age > 150 return false return true
Correct:
validateAge() as pure -> age as Integer <- valid <- false maxAge <- 150 valid: age >= 0 and age <= maxAge
E50060 — String has no toUpperCase() method. Use upperCase() instead. See ek9 -h E50060 for details.
Incorrect:
category <- categorise(score).toUpperCase()
Correct:
category <- categorise(score)
E01073 — EK9 has no null. Use an unset value (String()) to signal 'not found'. The caller uses a guard expression: if result <- findItem(name). See ek9 -h E01073 for details.
Incorrect:
<- found as String: null
Correct:
<- found as String: String()
Other ways to ask this
- How do I replace early returns with function decomposition in EK9?
- How do I avoid deeply nested if/else in EK9 without return?
- What is the decomposition pattern for complex functions in EK9?
- How do I write focused single-purpose functions instead of using early returns?
Coming from another language?
Java: early return for validation, guard clauses common, Extract Method refactoring in IDE. Python: early return common, guard clauses with if/return at top of function. Rust: early return with ?, pattern matching replaces some guard clauses. Go: early return for error checking (if err != nil return), very common pattern. Kotlin: early return, when expression reduces some nesting. Swift: guard let for early return on nil. EK9: no return statement, decompose into focused functions, guard expressions for conditional execution, unset return as signal, compiler verifies all paths initialise return variable.
Keywords: guard, flow, validate, null-safe, return, single, early, refactor, process, exit, path, compose, condition, function, focused, control, decomposition, safe, isset, branch, nested