skip to content

You inline a function that runs its lambda inside a nested `Runnable` object. Why does the compiler force `crossinline`, and how does that change non-local returns?

level: seniorimportance: should knowfreq 35%

answer

  1. Lambda called from nested object => crossinline required
  2. crossinline = inlined but no non-local return
  3. Error: 'may contain non-local returns'
  4. Spectrum: plain (yes/yes), crossinline (yes/no), noinline (no/no)
  5. Labelled return@ still allowed under crossinline

basics

~20 s

If the inlined lambda is called from inside another object (like a Runnable), it could run later, so a non-local return would be unsafe. The compiler makes you mark the lambda crossinline, which keeps it inlined but bans bare return from it.

solid answer

~40 s

When an `inline` function passes its lambda into a **nested execution context** — e.g., constructing a `Runnable` whose `run` body invokes the lambda — the lambda might execute outside the inline function's own control flow (later, or on another thread). Allowing a non-local `return` there would attempt to return from a stack frame that may no longer exist, so the compiler rejects it: "Can't inline 'block' here: it may contain non-local returns." The fix is to mark the parameter **`crossinline`**. This keeps the lambda inlined (no `Function` object), preserving performance, but **forbids non-local returns** from it; only labelled `return@label` is allowed. So `crossinline` is the middle ground between full inlining (non-local returns OK) and `noinline` (no inlining at all). It is common in builders, executors, and listener-registration helpers.

code

kotlin · 9 lines
kotlin
inline fun later(crossinline block: () -> Unit) {
    val task = object : Runnable { override fun run() = block() }
    enqueue(task)
}

fun use() {
    later { println("hi"); return@later } // OK, local
    // later { return }                    // compile error without/with crossinline
}

go deeper

for a junior

May only know crossinline exists; unlikely to explain the nested-context trigger.

for a middle

States that crossinline keeps inlining but bans non-local returns.

for a senior

Explains the nested-execution-context cause, the safety rationale, and the plain/crossinline/noinline spectrum precisely.

for a principal

Weighs API ergonomics: documents return semantics for callers and chooses crossinline vs. noinline vs. dropping inline by use case.

## The setup Consider an inline helper that schedules work: ```kotlin inline fun runOnUi(crossinline block: () -> Unit) { val r = Runnable { block() } // block invoked from inside an anonymous object postToMainThread(r) } ``` The lambda `block` is invoked from **inside the `Runnable` object's `run`**, not directly in `runOnUi`'s linear body. After inlining, `block`'s code is pasted into the `Runnable`. If `block` contained a bare `return`, it would try to return from `runOnUi` — but by the time the `Runnable` runs (possibly on another thread, later), `runOnUi` has already returned. That frame is gone. ## Why the compiler intervenes Without a marker, the compiler would error: ``` error: can't inline 'block' here: it may contain non-local returns. Add 'crossinline' modifier to parameter declaration 'block' ``` This is a **compile-time** safety guarantee — the language refuses to generate code that could unwind a dead frame. ## What `crossinline` does `crossinline`: - **keeps the lambda inlined** (no allocated `Function` object → same performance benefit), - **disallows non-local returns** from it (bare `return` becomes a compile error), - still permits **labelled local returns** (`return@runOnUi`). So inside a `crossinline` lambda: ```kotlin runOnUi { return@runOnUi } // OK: local runOnUi { return } // compile error ``` ## The three-way spectrum | Modifier | Inlined? | Non-local return? | |---|---|---| | (plain inline param) | yes | yes | | `crossinline` | yes | no | | `noinline` | no | no | - **plain**: lambda invoked directly in the function body; non-local returns are safe. - **`crossinline`**: lambda invoked from a nested context; inlined but non-local returns banned. - **`noinline`**: lambda treated as a real object (e.g., stored or passed to another function); not inlined. ## Practical guidance - Reach for `crossinline` when your inline function wraps the lambda in a `Runnable`, `Comparator`, callback object, or passes it to another (non-inline) function while you still want inlining. - Document that callers cannot early-return the outer function from such lambdas. - If you actually need to store the lambda for later, prefer `noinline` (or drop `inline` entirely).

  • Does crossinline cost a Function allocation like noinline?
    No. crossinline still inlines the lambda body, so there is no Function object allocated; it only removes the non-local-return capability.
  • What is the exact compiler message that hints you need crossinline?
    "Can't inline '<param>' here: it may contain non-local returns. Add 'crossinline' modifier to parameter declaration."

crossinline is a passport that lets the lambda travel into a nested object but revokes its right to return from the outer function.

saying these in an interview costs you the question

  • Claiming crossinline allocates a Function object like noinline
  • Saying crossinline allows non-local returns
  • Not recognizing the nested-object invocation as the trigger
  • Confusing the fix with noinline when inlining is still desired
  • Believing this is a runtime check rather than compile-time

context