Skip to main content
kotlin in depth advanced patterns for java engineers

Contracts, Crossinline, and Noinline

11 min read Chapter 9 of 21

Contracts, Crossinline, and Noinline

Inline Function Compilation

What inline Actually Does

When you mark a function inline, the compiler replaces every call site with the function’s body, and every lambda argument’s body is spliced directly into the generated code. No function call. No Function object allocation.

Consider this function:

inline fun measure(block: () -> Unit) {
    val start = System.nanoTime()
    block()
    val elapsed = System.nanoTime() - start
    println("Took ${elapsed}ns")
}

When you call it:

fun processData() {
    measure {
        database.query("SELECT * FROM users")
        transformResults()
    }
}

The compiler generates bytecode equivalent to:

fun processData() {
    val start = System.nanoTime()
    database.query("SELECT * FROM users")
    transformResults()
    val elapsed = System.nanoTime() - start
    println("Took ${elapsed}ns")
}

The measure function is gone. The lambda body is pasted in place. If you decompile the bytecode, you’ll find no Function0 allocation, no invoke() call, no trace that measure ever existed.

In Java, the closest equivalent is:

void processData() {
    long start = System.nanoTime();
    Runnable block = () -> {
        database.query("SELECT * FROM users");
        transformResults();
    };
    block.run();
    long elapsed = System.nanoTime() - start;
    System.out.println("Took " + elapsed + "ns");
}

Even with JIT optimization, the JVM may inline the lambda — but it’s not guaranteed. Kotlin’s inline keyword makes it a compile-time guarantee. The cost is always zero.

Non-Local Returns

Inlining introduces a behavior that doesn’t exist in Java: non-local returns. Because the lambda body is copied into the enclosing function, a return inside the lambda returns from the enclosing function, not from the lambda.

fun findFirstNegative(numbers: List<Int>): Int? {
    numbers.forEach { number ->    // forEach is inline
        if (number < 0) {
            return number          // returns from findFirstNegative, not from forEach
        }
    }
    return null
}

After inlining, this becomes:

fun findFirstNegative(numbers: List<Int>): Int? {
    for (number in numbers) {
        if (number < 0) {
            return number    // clearly returns from findFirstNegative
        }
    }
    return null
}

The return inside the lambda acts as if the lambda’s boundary doesn’t exist — because after inlining, it literally doesn’t. This is what “non-local” means: the return exits a scope beyond the lambda itself.

In a non-inline lambda, this is impossible:

fun process(numbers: List<Int>, action: (Int) -> Unit) {  // not inline
    numbers.forEach { action(it) }
}

fun caller() {
    process(listOf(1, -2, 3)) { number ->
        if (number < 0) return  // ERROR: 'return' is not allowed here
    }
}

The lambda is an actual object. Its code runs inside Function1.invoke(). A return would exit invoke(), but the compiler can’t guarantee that would meaningfully exit caller(). So it’s prohibited.

crossinline: Blocking Non-Local Returns

Sometimes you write an inline function but pass the lambda into a context where non-local returns are structurally impossible — like launching it on another thread, wrapping it in a Runnable, or storing it for later execution.

inline fun runOnUiThread(block: () -> Unit) {
    val runnable = Runnable { block() }  // ERROR: can't inline 'block' here
    handler.post(runnable)
}

The problem: block is inlined, which means non-local returns are allowed inside it. But block() is called inside a Runnable — an object that will execute later, possibly on a different thread. A non-local return would try to return from a function that has already completed. That’s undefined behavior.

The fix is crossinline. It tells the compiler: “inline this lambda’s body, but prohibit non-local returns.”

inline fun runOnUiThread(crossinline block: () -> Unit) {
    val runnable = Runnable { block() }  // OK: block is inlined but can't use non-local return
    handler.post(runnable)
}

fun updateUI() {
    runOnUiThread {
        textView.text = "Updated"
        // return  // ERROR: 'return' is not allowed here (crossinline)
    }
}

The lambda body is still inlined — no Function object for block. But the compiler enforces that return inside block can only be a local return (using return@runOnUiThread), never a non-local one.

When do you need crossinline? Whenever an inline function passes its lambda parameter to another lambda or stores it in an object:

inline fun <T> Iterable<T>.parallelForEach(crossinline action: (T) -> Unit) {
    val scope = CoroutineScope(Dispatchers.Default)
    forEach { element ->
        scope.launch {
            action(element)  // executed in a coroutine — non-local return is impossible
        }
    }
}

Without crossinline, the compiler rejects this because action is used inside a coroutine lambda (launch { }), which is a different execution context.

noinline: Keeping the Lambda as an Object

The opposite situation: sometimes you need the lambda to remain a Function object. Maybe you want to store it in a collection, pass it to a non-inline function, or return it from the current function.

inline fun setupCallbacks(
    onSuccess: () -> Unit,
    noinline onError: (Exception) -> Unit  // kept as a Function object
) {
    // onSuccess is inlined — its body is copied here
    onSuccess()

    // onError stays as a Function1 instance — can be stored, passed around
    errorHandler.register(onError)
}

Without noinline, the compiler won’t let you pass onError to errorHandler.register() because inline lambdas don’t exist as objects — their body is spliced into the call site. There’s nothing to pass.

Use noinline when:

  • Storing the lambda in a field or collection
  • Passing it to a function that isn’t inline
  • Returning it from the inline function
  • Using it as an argument to a function that expects a Function type
inline fun <T> buildPipeline(
    noinline transform: (T) -> T,
    noinline validate: (T) -> Boolean
): Pipeline<T> {
    // Both lambdas must be objects because they're stored in Pipeline
    return Pipeline(transform, validate)
}

A function where every lambda parameter is noinline gains nothing from being inline itself (unless it uses reified type parameters). The compiler will warn you about this.

Kotlin Contracts

Contracts are Kotlin’s mechanism for communicating information to the compiler that it can’t infer on its own. They’re declared inside a function body using the contract { } block and affect control flow analysis, smart casts, and variable initialization checks.

callsInPlace: Telling the Compiler How Often a Lambda Runs

The most common contract. Consider this code:

fun initialize() {
    val name: String
    run {
        name = "Kotlin"  // ERROR without contract: val might be reassigned
    }
    println(name)         // ERROR without contract: val might not be initialized
}

Without a contract, the compiler can’t know if the lambda passed to run executes zero times, once, or multiple times. If it runs zero times, name is uninitialized at println. If it runs twice, name is assigned twice — illegal for a val. So the compiler rejects both lines.

Here’s how run declares its contract:

@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

callsInPlace(block, InvocationKind.EXACTLY_ONCE) tells the compiler: “I guarantee this lambda runs exactly one time.” Now the compiler knows name will be assigned exactly once — valid for val — and will definitely be initialized before println.

The four invocation kinds:

contract {
    callsInPlace(block, InvocationKind.EXACTLY_ONCE)   // run, with, apply, also
    callsInPlace(block, InvocationKind.AT_LEAST_ONCE)  // at least once
    callsInPlace(block, InvocationKind.AT_MOST_ONCE)   // zero or one time
    callsInPlace(block, InvocationKind.UNKNOWN)         // no guarantees (default)
}

returns: Enabling Smart Casts

The returns contract tells the compiler what conditions are true when the function returns normally (without throwing):

fun requireNotNull(value: Any?): Unit {
    contract {
        returns() implies (value != null)
    }
    if (value == null) throw IllegalArgumentException("Value must not be null")
}

After calling requireNotNull(x), the compiler knows x is not null:

fun process(input: String?) {
    requireNotNull(input)
    // After this line, input is smart-cast to String (non-null)
    println(input.length)  // no ?.  needed
}

This is how require(), check(), and checkNotNull() in the standard library enable smart casts. The contract bridges the gap between runtime validation and compile-time type narrowing.

In Java, there’s no way to express this. Even with Objects.requireNonNull(input), the compiler still treats input as @Nullable after the call. You need explicit casts or suppression annotations.

void process(@Nullable String input) {
    Objects.requireNonNull(input);
    // Java still considers input nullable here
    System.out.println(input.length());  // warning or error with strict null checking
}

Writing Your Own Contract Functions

You can define contracts on your own functions. A practical example — a transaction function that guarantees exactly-once execution:

inline fun <T> transaction(block: () -> T): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    val connection = connectionPool.acquire()
    try {
        connection.beginTransaction()
        val result = block()
        connection.commit()
        return result
    } catch (e: Exception) {
        connection.rollback()
        throw e
    } finally {
        connectionPool.release(connection)
    }
}

Usage:

fun transferFunds(from: Account, to: Account, amount: BigDecimal) {
    val receipt: TransferReceipt

    transaction {
        from.debit(amount)
        to.credit(amount)
        receipt = TransferReceipt(from, to, amount, Instant.now())
    }

    // receipt is guaranteed to be initialized here
    emailService.sendConfirmation(receipt)
}

Without the callsInPlace contract, the compiler would reject receipt as potentially uninitialized after the transaction block. The contract communicates: “the block runs exactly once, so any val assigned inside it is definitely initialized.”

Another example — a conditional execution function with a returns contract:

inline fun assertAuthorized(user: User?, permission: Permission) {
    contract {
        returns() implies (user != null)
    }
    if (user == null || !user.hasPermission(permission)) {
        throw UnauthorizedException("User lacks permission: $permission")
    }
}

fun deleteResource(user: User?, resourceId: String) {
    assertAuthorized(user, Permission.DELETE)
    // user is smart-cast to non-null User here
    auditLog.record(user.id, "deleted $resourceId")
}

Contract Limitations

Contracts are currently experimental (@ExperimentalContracts) and have real restrictions:

  1. Only top-level functions — you can’t declare contracts on class methods or local functions
  2. First statement — the contract { } block must be the first statement in the function body
  3. Not verified — the compiler trusts your contract. If you declare EXACTLY_ONCE but call the block twice, you get undefined behavior. The compiler does not check your implementation against the contract
  4. Limited expressions — only callsInPlace and returns() implies (condition) are supported. You can’t express “this function never returns” or “this parameter is modified”
// This compiles but is WRONG — the contract lies
inline fun broken(block: () -> Unit) {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    // Calling block twice violates the contract
    block()
    block()  // compiler doesn't catch this
}

The contract system depends on developer honesty. Treat contracts like unsafe blocks in Rust — they bypass compiler checks and place the correctness burden on you.

Performance: When Inlining Helps vs. Hurts

Inlining eliminates lambda allocation and virtual dispatch. For small, frequently-called functions — scope functions, DSL builders, collection operations — the benefit is clear: zero overhead abstraction.

But inlining has a cost: code size. Every call site gets a copy of the function body and every lambda body. A large inline function called from 50 places generates 50 copies of its bytecode.

// Good candidate for inline: small body, takes lambda
inline fun <T> logged(tag: String, block: () -> T): T {
    log.debug("[$tag] start")
    val result = block()
    log.debug("[$tag] end")
    return result
}

// Bad candidate for inline: large body
inline fun processLargeDataset(data: List<Record>, transform: (Record) -> Output): List<Output> {
    // 200 lines of processing logic
    // Every call site copies all 200 lines
    // JVM's own inliner may be more selective and efficient
}

Guidelines:

  • Inline functions with lambda parameters where avoiding allocation matters
  • Inline small utility functions (1-5 lines) used frequently
  • Don’t inline large functions without lambda parameters — let the JIT handle it
  • Don’t inline functions that call other large inline functions — the expansion compounds

The compiler warns if you mark a function inline but it has no lambda parameters and doesn’t use reified types. Heed that warning. Inlining for the sake of inlining can increase your APK/JAR size and hurt instruction cache performance — the opposite of what you intended.

The Full Inline Spectrum at a Glance

inline fun example(
    block: () -> Unit,              // inlined, non-local return allowed
    crossinline restricted: () -> Unit,  // inlined, non-local return prohibited
    noinline kept: () -> Unit       // NOT inlined, remains a Function object
) {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }

    block()                         // body spliced here, can return from caller

    val runnable = Runnable {
        restricted()                // body spliced here, can't return from caller
    }

    someOtherFunction(kept)         // passed as Function0 object
}

Each modifier addresses a specific constraint:

  • Default (no modifier): maximum optimization, full non-local return capability
  • crossinline: inlined but used in a context where non-local returns are structurally impossible
  • noinline: the lambda must exist as an object — storage, passing to non-inline code, or reflection

Java has none of these controls. Lambda optimization is entirely at the JVM’s discretion, with no way for the developer to communicate intent, guarantee inlining, or influence control flow analysis. Kotlin’s inline system gives you explicit control over the compilation strategy of higher-order functions — and the contracts system lets you feed that information back to the type checker.