skip to content

Smart Casts

How the compiler narrows a type for you after a null check or is-check, so you can use the value without casting. Equally important is what defeats it — mutable properties, values from other modules — which is a favorite follow-up.

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

explore

questions

15

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

What is a smart cast in Kotlin, and how does it work after an `if (x != null)` or `is` check?

level: juniorimportance: must knowfreq 80%

basics

~10 s

After you check a variable for null or for a type, Kotlin automatically treats it as the checked type inside that block. You can use it without writing an explicit cast.

open as a page

Why does Kotlin let you smart-cast a nullable `val` after a null check but refuse to do the same for a `var`?

level: juniorimportance: must knowfreq 70%

basics

~20 s

A val can never change, so after you check it is not null the compiler trusts it stays not null. A var can be reassigned, maybe to null, so the compiler cannot trust the check still holds.

open as a page

How does smart casting work with early returns and negated checks (e.g. `if (x == null) return`)?

level: middleimportance: must knowfreq 70%

basics

~10 s

If you return early when the value is null, the compiler knows that after that point the value cannot be null, so it smart-casts it to non-null for the rest of the function.

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

How do `&&` and `||` enable smart casting within a single boolean expression?

level: middleimportance: should knowfreq 60%

basics

~20 s

With &&, a check on the left makes the variable narrowed on the right, because the right side only runs if the left was true. With ||, the right side runs only when the left was false, so the opposite narrowing applies.

open as a page

A `val` property has a custom getter that may return different values on each access. Why does Kotlin refuse to smart-cast it even though it is declared `val`?

level: middleimportance: should knowfreq 55%

basics

~10 s

A val with a custom getter is really a function call every time you read it. Two reads can return different values, so checking once does not guarantee the next read is still non-null.

open as a page

Explain why an `open val` property on a class cannot be smart-cast, and how this relates to overriding.

level: middleimportance: should knowfreq 45%

basics

~20 s

An open val can be overridden by a subclass, and the override might use a custom getter that returns different values each read. So the compiler cannot assume reads are stable and blocks the smart cast.

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

Compare a smart cast after `if (x != null)` with the `!!` not-null assertion and with `?.`/`let`. When is each appropriate?

level: seniorimportance: should knowfreq 50%

basics

~20 s

A smart cast lets the compiler prove the value is non-null after a check, with no risk. !! forces non-null and crashes if you're wrong. ?./let handle the null case gracefully. Prefer smart cast or safe calls; use !! rarely.

open as a page

How do smart casts behave across `when` branches, including `when (val v = ...)` subjects and exhaustiveness?

level: seniorimportance: should knowfreq 55%

basics

~10 s

In a when, each branch automatically narrows the value to whatever that branch tested for — a type or non-null check — so you can use it directly inside that branch.

open as a page

Why can a `val` property defined in another module fail to smart-cast even when it looks like a plain field-backed property in your IDE?

level: seniorimportance: should knowfreq 35%

basics

~10 s

Code in another module is compiled separately. The compiler treats its properties conservatively because the implementation could change or use a getter it can't fully trust, so it won't risk a smart cast.

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

Compare the practical workarounds for non-smart-castable properties (local `val`, `?.let`, `!!`, `requireNotNull`) and explain the tradeoffs a code reviewer should weigh.

level: seniorimportance: nice to knowfreq 25%

basics

~20 s

Copy the value into a local val to keep it readable and safe, or use ?.let for a quick block. Use requireNotNull/checkNotNull when null is a real error. Avoid !! because it just throws on null and hides bugs.

open as a page