skip to content

What does the crossinline modifier do on a lambda parameter of an inline function?

level: juniorimportance: must knowfreq 55%

answer

  1. Inlined but no non-local return
  2. Needed when lambda runs in another context (Runnable/nested lambda)
  3. Only return@label allowed inside
  4. Middle ground: inline < crossinline < noinline
  5. Compiler error without it when capturing the lambda

basics

~20 s

crossinline marks a lambda of an inline function so it cannot use a non-local return. It is still inlined, but you are not allowed to write 'return' to exit the calling function from inside it.

solid answer

~40 s

In an inline function, lambda parameters are inlined into the call site, which lets them use non-local returns: a plain 'return' inside the lambda returns from the enclosing function. crossinline keeps the lambda inlined but forbids that non-local return. You apply it when the lambda is not called directly inside the inline function body but is instead passed into another execution context, for example stored in a Runnable, an object, or a nested lambda, where allowing a return-from-caller would be impossible or unsafe. The lambda body is still inlined (no separate function object created for the simple path), so you keep inlining benefits, but only local 'return@label' returns are permitted. It is the middle ground between a normal inline lambda (non-local return allowed) and noinline (not inlined at all).

code

kotlin · 11 lines
kotlin
inline fun runLater(crossinline body: () -> Unit) {
    val r = Runnable { body() }   // body invoked in another execution context
    r.run()
}

fun demo() {
    runLater {
        // return        // ERROR: non-local return forbidden
        return@runLater   // OK: local return only
    }
}

go deeper

for a junior

Can state that crossinline keeps the lambda inlined but blocks non-local returns and is needed when the lambda is captured elsewhere.

for a middle

Explains the call-site-copying mechanism behind non-local returns and why capturing into a Runnable breaks it.

for a senior

Contrasts crossinline vs noinline vs plain inline precisely and picks the right one for a given indirect-invocation scenario.

for a principal

Reasons about codegen and API ergonomics: when to expose crossinline vs noinline in a public inline DSL to balance inlining wins against caller flexibility.

## The problem crossinline solves When a function is marked `inline`, the compiler copies the function body and its lambda arguments directly into the call site. Because the lambda body ends up physically inside the caller, a bare `return` inside that lambda can return from the **enclosing function** — this is called a **non-local return**. ```kotlin inline fun forEachItem(list: List<Int>, action: (Int) -> Unit) { for (item in list) action(item) } fun find(list: List<Int>): Int { forEachItem(list) { item -> if (item > 10) return item // non-local return: returns from find() } return -1 } ``` This works because `action` is invoked **directly** inside `forEachItem`'s own body. But sometimes you don't call the lambda directly — you pass it somewhere else (another lambda, a `Runnable`, an anonymous object). In that case a non-local return is impossible: by the time the lambda runs, the caller's stack frame may be gone. ## What crossinline does `crossinline` tells the compiler: "keep inlining this lambda, but **forbid non-local returns** from it." The lambda is still inlined (so you avoid allocating a function object on the simple path), but only **local returns** (`return@label`) are allowed. ```kotlin inline fun runLater(crossinline body: () -> Unit) { val r = Runnable { body() } // body used in another execution context r.run() } ``` Without `crossinline`, the compiler would reject `Runnable { body() }` because the lambda is captured into another context where a non-local return cannot be honored. With `crossinline` the code compiles, and any caller's lambda may only do local returns: ```kotlin runLater { // return // COMPILE ERROR: 'return' not allowed here return@runLater // OK: local return, exits only this lambda } ``` ## How it differs from the alternatives - **Plain inline lambda** — inlined, non-local return allowed (the default for inline-function lambdas). - **crossinline lambda** — inlined, non-local return **forbidden**; needed when the lambda runs in a different execution context. - **noinline lambda** — **not** inlined at all; becomes a real function object you can store/pass freely; non-local return also impossible. ## Key keywords and APIs involved - `inline`, `crossinline`, `noinline` modifiers - non-local `return` vs local `return@label` - function types like `() -> Unit`, used as `Runnable`, captured in nested lambdas or anonymous objects ## Mental rule Use `crossinline` when you want the lambda inlined **and** you must use it indirectly (inside a `Runnable`, a callback object, a nested lambda). If you instead need to store it in a field or pass it onward as a value, use `noinline`.

  • Is a crossinline lambda still inlined?
    Yes. crossinline only removes the non-local return capability; the body is still inlined into the call site, so you keep inlining benefits.
  • What return is still legal inside a crossinline lambda?
    A labeled local return such as return@functionName, which exits only the lambda itself.

Like a guest you bring into a room but who isn't allowed to flip the building's main breaker — they can only switch off the lamp in their own corner.

saying these in an interview costs you the question

  • Saying crossinline turns off inlining (it does not — that's noinline)
  • Claiming crossinline allows non-local returns
  • Confusing crossinline with noinline
  • Believing crossinline is about performance rather than control flow
  • Thinking it forbids all returns, including local return@label

context