How do requireNotNull/checkNotNull enable a smart cast to non-null afterward, and what stdlib mechanism makes that possible? How does this differ from wrapping a Java value in your own helper?
answer
- Stdlib guards use kotlin.contracts: returns() implies (value != null)
- Smart-cast applies to the original variable, not just return value
- Your own checker won't smart-cast without its own contract
- Contracts API is experimental -> @OptIn(ExperimentalContracts::class)
- They both validate and return the value
basics
~20 sThese functions are declared with Kotlin contracts that tell the compiler 'if this returns, the value is not null'. So after calling them you can use the original variable as non-null. A plain helper you write won't smart-cast unless you also declare a contract.
solid answer
~40 s`requireNotNull`/`checkNotNull` (and `require`/`check`) use Kotlin's **contracts API** (`kotlin.contracts`). Their bodies declare `contract { returns() implies (value != null) }`, which promises the compiler: *if the function returns normally, `value` is non-null*. The compiler propagates that as a **smart cast**, so after the call the original variable is treated as non-null for the rest of the scope — not just the returned value. If you write your own `fun myRequire(x: Any?)` and throw when null, the compiler does **not** know the post-condition, so the variable stays nullable afterward — unless you add the same `contract { ... }` block (an experimental but stable-in-practice feature) with `@OptIn(ExperimentalContracts::class)`. This is why preferring the stdlib guards at a boundary is ergonomic: one call both validates and unlocks non-null usage.
code
kotlin · 12 linesimport kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
@OptIn(ExperimentalContracts::class)
fun <T : Any> mustExist(x: T?, name: String): T {
contract { returns() implies (x != null) }
return x ?: throw IllegalArgumentException("$name is null")
}
val id: String? = javaUser.id
mustExist(id, "id")
id.length // smart-cast to non-nullgo deeper
Knows that after requireNotNull you can use the value as non-null.
Knows the original variable is smart-cast, not just the returned value.
Explains the kotlin.contracts mechanism and can write a contract-backed custom guard.
Weighs adding shared contract-backed validation helpers vs. relying on stdlib, considering opt-in/experimental status and API-stability risk across the codebase.
## What smart casting means A **smart cast** is the compiler automatically treating a value as a more specific type after a check, with no explicit cast. After `if (x != null) { /* x is non-null here */ }`, `x` is smart-cast inside the branch. ## How the stdlib guards smart-cast across a call `requireNotNull`, `checkNotNull`, `require`, `check` are declared with the **Kotlin contracts API** in `kotlin.contracts`. Their declarations include something like: ```kotlin public inline fun <T : Any> requireNotNull(value: T?, lazyMessage: () -> Any): T { contract { returns() implies (value != null) } if (value == null) throw IllegalArgumentException(lazyMessage().toString()) return value } ``` The line `contract { returns() implies (value != null) }` tells the compiler: *if this call returns normally (doesn't throw), then `value` is non-null*. The compiler uses that to **smart-cast the argument variable** at the call site: ```kotlin val raw: String? = javaCall() requireNotNull(raw) { "raw missing" } raw.length // OK: raw is smart-cast to String here ``` Note you didn't even have to capture the return value — the original `raw` is now non-null in scope. (They *also* return the value so `val v = requireNotNull(raw)` works.) ## Your own helper does NOT smart-cast — unless you add a contract ```kotlin fun ensure(x: Any?) { if (x == null) error("null") } val r: String? = javaCall() ensure(r) r.length // ERROR: r is still String?; compiler has no contract ``` To make your helper behave like the stdlib, declare the contract yourself: ```kotlin import kotlin.contracts.ExperimentalContracts import kotlin.contracts.contract @OptIn(ExperimentalContracts::class) fun <T : Any> ensureNotNull(x: T?): T { contract { returns() implies (x != null) } return x ?: error("null") } ``` Now `ensureNotNull(r)` smart-casts `r` to non-null afterward. The contracts API is still marked **experimental** (requires opt-in) but the relevant forms are widely used and stable in practice. ## Why this matters at the interop boundary A platform value like `String!` is treated as nullable when you assign it to `String?`. Running it through a contract-backed guard both **enforces** non-nullity (throwing with a meaningful exception) and **informs** the compiler, so the rest of your boundary code uses the value with no `!!`, no repeated `?.`, and no casts. Rolling your own checker without a contract loses that ergonomic payoff. ## Key terms - **Contracts API** (`kotlin.contracts`): lets a function declare effects (e.g. `returns() implies (...)`, `callsInPlace`) the compiler can reason about. - **`returns() implies (cond)`**: 'if this returns normally, `cond` holds.' - **Smart cast**: compiler-inserted, automatic narrowing of a type after a proven condition.
- After `requireNotNull(x)`, can you use `x` directly without the return value?Yes. The contract smart-casts the original variable to non-null in scope, so `x` is usable directly; the return value is a convenience for inline assignment.
- Why won't a hand-written `assertNonNull(x)` smart-cast x by default?The compiler doesn't analyze arbitrary function bodies for post-conditions. Without an explicit contract { returns() implies (x != null) }, it can't conclude x is non-null after the call.
saying these in an interview costs you the question
- Claiming any function that throws on null auto-enables smart cast
- Not knowing the contracts API underlies require/check
- Thinking only the return value (not the variable) becomes non-null
- Unaware the contracts API needs @OptIn
- Believing smart cast is a runtime cast rather than a compile-time narrowing