skip to content

Contract-Driven Smart Casts

Contracts let a function tell the compiler what its return value implies, which is how requireNotNull and isNullOrEmpty keep smart casts alive past the call. Interviewers bring this up when asking why your own helper does not narrow types the way the stdlib's does.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

questions

5

Why can you use a variable as non-null after calling requireNotNull(x) or checkNotNull(x), even though the null check happens inside a separate function?

level: juniorimportance: must knowfreq 55%

answer

  1. contract { returns() implies (value != null) }
  2. requireNotNull -> IllegalArgumentException (inputs)
  3. checkNotNull -> IllegalStateException (state)
  4. Both return the non-null value
  5. Smart cast survives past the call

basics

~20 s

Those standard functions tell the compiler a fact: if they return normally, the argument is not null. So after the call the compiler treats the variable as non-null and lets you use it without ?. or !!.

solid answer

~40 s

requireNotNull and checkNotNull are declared with a Kotlin contract. A contract is a compiler-readable promise attached to a function. Theirs says returns() implies (value != null): if the function returns normally (doesn't throw), the argument was non-null. The compiler reads that and smart-casts the variable from T? to T past the call site, so you can call members directly without ?., !!, or a local val. requireNotNull throws IllegalArgumentException on null (precondition on input); checkNotNull throws IllegalStateException (invariant on state). Both also return the non-null value, so val nn = requireNotNull(x) works too. Without the contract the compiler couldn't know the function guaranteed non-nullity and would still force null handling afterward.

code

kotlin · 5 lines
kotlin
fun greet(name: String?) {
    requireNotNull(name) { "name must be provided" }
    // name is smart-cast to String from here on
    println("Hello, ${name.uppercase()}")
}

go deeper

for a junior

Knows that after requireNotNull/checkNotNull the variable is usable as non-null and you don't need !!.

for a middle

Explains the contract returns() implies (value != null) and the IllegalArgumentException vs IllegalStateException distinction.

for a senior

Discusses fail-fast intent, choosing these over !! for clearer exceptions, and that the return value is the non-null type.

for a principal

Frames contract helpers as part of a nullability-validation policy and when fail-fast vs graceful (?.) is the right boundary behavior.

## The problem Kotlin's compiler tracks nullability. After an inline `if (x != null)` check it performs a **smart cast**: inside that branch `x: String?` is usable as `String`. But when the null check lives inside *another function*, the compiler normally can't see it — a function call is opaque. ```kotlin fun handle(name: String?) { // without help, compiler still thinks name is String? requireNotNull(name) println(name.length) // works: smart-cast to String here } ``` ## How the standard library makes this work: contracts A **contract** is metadata attached to a function that describes its effects in a form the compiler understands. `requireNotNull` and `checkNotNull` are declared (roughly) like: ```kotlin public inline fun <T : Any> requireNotNull(value: T?): T { contract { returns() implies (value != null) } return value ?: throw IllegalArgumentException("Required value was null.") } ``` - `contract { ... }` is the block (from `kotlin.contracts`) where effects are declared. It must be the first statement. - `returns()` means "the function returns normally" (no exception thrown). - `implies (value != null)` states the condition that holds *given* a normal return. So the compiler reasons: *the line after the call executed, therefore the function returned normally, therefore `value != null`* — and smart-casts the argument to its non-null type. ## requireNotNull vs checkNotNull - `requireNotNull(x)` throws **IllegalArgumentException** — use it to validate **inputs / arguments**. - `checkNotNull(x)` throws **IllegalStateException** — use it to validate **internal state / invariants**. - Both **return** the non-null value, so `val nn = checkNotNull(x)` is also valid. ## Contrast with !! and ?. - `x!!` throws `NullPointerException` and gives no descriptive message; the contract-based helpers throw clearer exceptions and read as intent. - `x?.foo()` skips when null instead of asserting non-null. Use the contract helpers when null is genuinely a bug you want to fail fast on with a clear message.

  • What is the difference between requireNotNull and checkNotNull?
    Same contract and smart-cast behavior; requireNotNull throws IllegalArgumentException (argument/precondition), checkNotNull throws IllegalStateException (internal-state/invariant).
  • Does requireNotNull return anything useful?
    Yes — it returns the non-null value, so you can write val nn = requireNotNull(x) and bind the smart-cast result to a new val.

Like a signed receipt: the function hands back proof 'this wasn't null', and the compiler trusts the receipt for the rest of the scope.

saying these in an interview costs you the question

  • Saying it's the same as x!! (different exception, no contract semantics, no message)
  • Claiming the compiler 'looks inside' the function body — it reads the contract, not the code
  • Mixing up which one throws IllegalArgumentException vs IllegalStateException
  • Thinking the smart cast only applies inside the function, not after the call

context

open as a page

Write a custom validation helper that smart-casts its argument to non-null after the call. Explain each part of the contract DSL you use.

level: middleimportance: should knowfreq 40%

basics

~20 s

Add a contract { } block as the first line of the function. Inside, write returns() implies (arg != null). That tells the compiler that whenever the function returns normally, the argument was not null, so callers can use it as non-null.

open as a page

How does isNullOrEmpty() let the compiler smart-cast a String? to String in only one branch of an if? Show the contract that makes it work.

level: middleimportance: should knowfreq 45%

basics

~20 s

isNullOrEmpty() carries a contract saying 'if I return false, the receiver is not null and not empty'. So in the else branch (or after a negated check) the compiler knows the string is non-null and smart-casts it.

open as a page

What are the limits and soundness risks of contract-based smart casts? When can a contract still fail to smart-cast, and how can a bad contract cause a runtime crash?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Contracts are promises the compiler trusts but never checks. If your function body doesn't really enforce the claim, you get an unsound smart cast that can crash at runtime. Smart casts also still fail on mutable or externally-changeable variables.

open as a page

Beyond null smart-casts, what other contract effects exist, and how do returns()/implies differ from callsInPlace? Give a contract that smart-casts to a non-null subtype.

level: seniorimportance: nice to knowfreq 18%

basics

~20 s

Kotlin contracts have two families: returns()/implies, which tells the compiler facts about arguments (like 'not null' or 'is this type') after a result, and callsInPlace, which tells it how often a lambda runs so you can initialize vals inside it.

open as a page