skip to content

What is a non-local return in Kotlin, and why does `return` inside `forEach { }` exit the enclosing function?

level: juniorimportance: must knowfreq 60%

answer

  1. Bare return = exits enclosing function
  2. Only works because the HOF is inline
  3. Non-inline HOF => bare return is a compile error
  4. return@forEach = leave the lambda only (like continue)
  5. inline = copy lambda body into call site

basics

~20 s

A plain return inside a lambda can jump out of the whole surrounding function, not just the lambda. This works in forEach because forEach is an inline function, so the lambda's body is copied directly into your code.

solid answer

~40 s

A non-local return is a bare `return` written inside a lambda that exits the function that *contains* the lambda, not the lambda itself. It is allowed only when the lambda is passed to an `inline` function. Because `inline` causes the compiler to copy both the function body and the lambda body straight into the call site, the `return` ends up textually inside the caller, so it returns from the caller. `kotlin.collections.forEach` is declared `inline`, so `list.forEach { if (it == x) return }` returns from the enclosing function. With a regular (non-inline) higher-order function the lambda is a real object; a bare `return` there is a compile error, and you must use a labelled `return@label` to return from the lambda only.

code

kotlin · 7 lines
kotlin
fun firstNegative(xs: List<Int>): Int? {
    xs.forEach { x ->
        if (x < 0) return x        // non-local: returns from firstNegative
        if (x == 0) return@forEach // local: skip, keep iterating
    }
    return null
}

go deeper

for a junior

Knows a bare return in forEach exits the function and return@forEach skips one element.

for a middle

Explains the link to inline and that non-inline HOFs forbid bare returns at compile time.

for a senior

Describes the copy-paste codegen mental model and the implicit-label mechanics precisely.

for a principal

Frames it as a deliberate language design tradeoff that makes inline HOFs feel like built-in control structures.

## What a non-local return is A **lambda** is a block of code you pass as an argument. Normally, control flow keywords like `return` only affect the function they are written in. A **non-local return** is a special Kotlin feature: a bare `return` written *inside a lambda* exits the **enclosing function** (the function that called the higher-order function), not the lambda. ```kotlin fun hasZero(numbers: List<Int>): Boolean { numbers.forEach { n -> if (n == 0) return true // returns from hasZero, not from the lambda } return false } ``` Here `return true` exits `hasZero` entirely — that is the "non-local" part: the return target is *outside* the lambda. ## Why it works only with `inline` `forEach` is declared in the standard library as an **`inline` function**. `inline` tells the compiler to **copy the function body and the lambda body directly into the call site** instead of creating a separate function object. After inlining, the `return` literally sits inside `hasZero`, so it returns from `hasZero`. With a **non-inline** higher-order function, the lambda becomes a real `Function` object that may run later or on another thread. A bare `return` there has no meaningful target, so Kotlin makes it a **compile error**: ```kotlin fun runIt(block: () -> Unit) { block() } // NOT inline fun demo() { runIt { return } // error: 'return' is not allowed here } ``` ## Returning from the lambda only: labels If you want to leave just the current lambda iteration (like `continue`), use a **labelled return**, `return@forEach`: ```kotlin list.forEach { if (it == 0) return@forEach // skip this element, keep looping } ``` The implicit label is the function name (`@forEach`); you can also name your own label. ## Key APIs / keywords - **`inline`** — enables non-local returns from lambdas passed to it. - **`return`** (bare) — non-local: exits the enclosing function. - **`return@label`** — local: exits only the lambda. - Standard-library inline HOFs that support non-local return: `forEach`, `let`, `also`, `run`, `apply`, `with`, `repeat`, `takeIf`. ## Mental model Think of `inline` as a copy-paste of the lambda into your function. Once pasted, a `return` is just an ordinary `return` in your own code.

  • How do you return from the lambda only, not the enclosing function?
    Use a labelled return such as `return@forEach`, which exits just the current lambda invocation, behaving like `continue` in a loop.
  • What error do you get with a bare return in a non-inline HOF?
    A compile error: "'return' is not allowed here" — because the lambda is a separate object with no enclosing-function return target.

Inlining is copy-pasting the lambda into your function, so its return becomes your function's return.

saying these in an interview costs you the question

  • Claiming a bare `return` in any lambda exits only the lambda
  • Thinking non-local returns work for all higher-order functions
  • Confusing `return` with `return@forEach`
  • Not knowing `forEach` is `inline`
  • Saying it is runtime reflection rather than compile-time inlining

context