Kotlin Contracts: Teach Your Compiler About Your Function’s Behavior

What Are Kotlin Contracts?

If you’ve worked with Kotlin for a while, you’ve likely encountered functions from the standard library that seem to have almost magical powers. Take require() and check()—call them with a condition, and the compiler somehow knows that if execution continues past that line, your variable is no longer nullable. Or use isNullOrEmpty() on a string, and suddenly the compiler treats it as non-null after the check. That’s not magic—that’s contracts.

Kotlin contracts are a powerful (but underutilized) feature that let you describe your function’s behavior to the compiler. They tell the compiler about the relationship between your function’s inputs, outputs, and side effects. When the compiler understands these relationships, it can make smarter decisions about type narrowing and null safety, all without requiring explicit casts.

A futuristic digital visualization of Kotlin code being refined through a glowing lens, representing how Kotlin Contracts help the compiler with type narrowing and null safety.
Making the “magic” of the Kotlin compiler visible through Contracts.

Understanding Contract Basics

A contract is declared inside your function using the contract() block. Here’s the simplest example:

@OptIn(ExperimentalContracts::class)
fun isNullOrEmpty(value: String?): Boolean {
    contract {
        returns(false) implies (value != null)
    }
    return value == null || value.isEmpty()
}

This contract tells the compiler: “If this function returns false, then value is definitely not null.” Notice the direction—we describe the false branch because the function returns true for both null and empty strings. We can only guarantee non-nullability when the result is false. Now when you write code like this, the compiler understands:

val name: String? = getName()
if (isNullOrEmpty(name)) {
    println("Name is empty or null")
    return
}
println(name.length) // Compiler knows name is not null here!

The compiler automatically narrows the type from String? to String after the guard clause. Since the early return handles the true case, the code below it only runs when isNullOrEmpty returned false—and your contract guarantees that means the value is not null.

Returns Condition Patterns

Contracts support several return value patterns. The most common are returns(true) and returns(false), but you can also use returns() (meaning “returns normally without throwing”) and returnsNotNull(). The value passed to returns() can only be true, false, or null—not arbitrary values:

fun validate(email: String?): Boolean {
    contract {
        returns(true) implies (email != null)
    }
    return email != null && email.contains("@")
}

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

fun findById(id: String?): User? {
    contract {
        returnsNotNull() implies (id != null)
    }
    if (id == null) return null
    return db.query(id)
}

An important constraint: the implies keyword can only describe nullability checks (like value != null) and type checks (like value is String). You cannot use arbitrary boolean conditions such as value > 0 or value.isNotEmpty()—the compiler will reject those.

CallsInPlace: Controlling Lambda Execution

Contracts aren’t limited to describing return values—they can also describe how your function executes its lambda parameters. The callsInPlace() effect tells the compiler about lambda execution patterns:

inline fun forEach(items: List, block: (String) -> Unit) {
    contract {
        callsInPlace(block, InvocationKind.UNKNOWN)
    }
    for (item in items) {
        block(item)
    }
}

The InvocationKind enum describes the calling pattern: AT_MOST_ONCE (maybe not called), AT_LEAST_ONCE (called at least once), EXACTLY_ONCE (called exactly once), or UNKNOWN (called zero or more times). Note that callsInPlace requires the function to be inline—the compiler needs to inline the lambda to verify the calling pattern. Here’s a practical example:

inline fun withLock(lock: Lock, block: () -> Unit) {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    lock.lock()
    try {
        block()
    } finally {
        lock.unlock()
    }
}

Because the contract says the lambda is called exactly once, the compiler allows you to initialize a val inside the lambda—something that would otherwise be rejected:

val result: String
withLock(myLock) {
    result = computeValue() // Works! Compiler trusts EXACTLY_ONCE
}
println(result) // Definitely initialized

Without the contract, the compiler would complain that result might not be assigned—or might be assigned twice. This definite initialization guarantee is the most practical benefit of callsInPlace.

Real-World Examples from the Standard Library

The Kotlin standard library uses contracts extensively. Here’s how require() is actually implemented:

inline fun require(value: Boolean, lazyMessage: () -> String) {
    contract {
        returns() implies value
        callsInPlace(lazyMessage, InvocationKind.AT_MOST_ONCE)
    }
    if (!value) {
        throw IllegalArgumentException(lazyMessage().toString())
    }
}

The contract says: “If this function returns (doesn’t throw), then value must be true.” This is why you can write:

val user: User? = fetchUser()
require(user != null) { "User not found" }
println(user.name) // Compiler knows user is not null

Similarly, runCatching uses a callsInPlace(block, EXACTLY_ONCE) contract so you can safely initialize variables inside its lambda. The library’s check(), checkNotNull(), and other validation functions use returns() implies contracts to provide smart type narrowing without explicit casts.

Writing Your Own Contract Functions

You can write contracts for your own functions too. Here’s a practical example for a custom validation function:

fun validateInput(input: Any?): Boolean {
    contract {
        returns(true) implies (input is String)
    }
    return input is String && input.contains("@") && input.contains(".")
}

fun processEmail(input: Any?) {
    if (validateInput(input)) {
        // Compiler knows input is a String — smart cast works!
        println(input.length)
    }
}

You can also combine multiple implications in a single contract:

fun validateUser(user: User?): Boolean {
    contract {
        returns(true) implies (user != null)
    }
    return user != null && user.age >= 18
}

For a deeper dive into functional error handling patterns that pair well with contracts, check out our guide to runCatching and Result. And if you want to explore other advanced Kotlin features that reduce boilerplate, take a look at property delegates with observable and vetoable for a different approach to controlling property behavior.

Important Limitations

Contracts are powerful, but they come with constraints. First, they’re currently marked as an experimental API—you’ll need to opt in with @OptIn(ExperimentalContracts::class). Second, the logic inside your contract block must be pure—no actual side effects. The block is checked by the compiler only; it doesn’t execute at runtime. Third, the implies clause only accepts nullability checks and type checks—the compiler rejects arbitrary boolean expressions like value > 0 or method calls like value.isNotEmpty().

Why This Matters for Android Development

On Android, contracts shine when you’re building validation layers, custom DSLs, or argument-checking utilities. They let you write safer code without defensive null checks and unnecessary casts. When you teach the compiler about your function’s guarantees, you reduce boilerplate and make your intent crystal clear to both the compiler and your teammates.

Keep in mind that contracts are a compile-time mechanism—they don’t generate different bytecode or add runtime overhead. Their value is purely in helping the compiler understand your code better, which means fewer explicit casts and null checks in your source code.


This post was written by a human with the help of Claude, an AI assistant by Anthropic.

Scroll to Top