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