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