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?
answer
- Lambda called from nested object => crossinline required
- crossinline = inlined but no non-local return
- Error: 'may contain non-local returns'
- Spectrum: plain (yes/yes), crossinline (yes/no), noinline (no/no)
- Labelled return@ still allowed under crossinline
basics
~20 sIf 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 sWhen 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 linesinline 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
May only know crossinline exists; unlikely to explain the nested-context trigger.
States that crossinline keeps inlining but bans non-local returns.
Explains the nested-execution-context cause, the safety rationale, and the plain/crossinline/noinline spectrum precisely.
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