skip to content

Smart-Cast Limitations

Smart casts fail whenever the compiler cannot prove the value has not changed since the check — a var, an open or custom-getter property, anything from another module. The expected answer is the local val workaround, and knowing why it is sound.

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

questions

5

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%

answer

  1. val = assigned once = stable = smart-castable
  2. var = mutable = could change = no smart cast
  3. Compiler reasons statically, not about your runtime path
  4. Fix: copy into a local val, or use ?.let / ?: return

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.

solid answer

~40 s

Smart casts work only when the compiler can prove the value is unchanged between the check and the use. A local `val` is assigned once, so after `if (x != null)` the compiler narrows the type from `String?` to `String` (smart cast) for the rest of that branch. A `var` can be reassigned anywhere, so the compiler cannot guarantee it is still non-null at the use site and refuses the smart cast, forcing you to use `?.`, `!!`, or copy into a local `val`. The proof is purely static: even if no reassignment exists at runtime, the mere fact that `var` is mutable blocks the cast. The standard fix is to capture into a local `val` (`val v = x ?: return`) and operate on that.

code

kotlin · 6 lines
kotlin
var name: String? = fetch()
// val local copy makes the smart cast provable
val local = name
if (local != null) {
    println(local.uppercase()) // OK: local is a stable val
}

go deeper

for a junior

Knows val gets smart-cast after a null check and var does not, and can apply the local-val fix.

for a middle

Explains stability and that the analysis is static/conservative, and reaches for ?.let or ?: return idiomatically.

for a senior

Frames it as the compiler proving immutability between check and use, and connects it to thread-safety motivations.

for a principal

Can reason about why conservative static analysis is the right default and where contracts or local copies trade safety for ergonomics.

## What a smart cast is A **smart cast** is a Kotlin compiler feature: after you check a value's type or nullability, the compiler automatically treats the value as the narrower type without an explicit cast. For example: ```kotlin fun length(s: String?): Int { if (s != null) { // s is smart-cast from String? to String here return s.length // no ?. needed } return 0 } ``` ## The core rule: stability A smart cast is only allowed on a **stable** value — one the compiler can prove will not change between the check and the use. A local `val` is stable: it is assigned exactly once and can never be reassigned. So after `if (s != null)` the compiler knows `s` is still non-null on the next line. ## Why `var` breaks it A `var` is mutable. Between the null check and the use, the value could be reassigned — possibly to `null`: ```kotlin var s: String? = "hi" if (s != null) { // Smart cast to String is NOT allowed here println(s.length) // compile error: smart cast impossible } ``` The compiler reasons statically (about all possible programs), not about your specific runtime path. Because a `var` *could* be reassigned, it is treated as unstable. This is conservative on purpose: it prevents a class of bugs where a value is nulled out between check and use (especially across threads). ## The standard workaround: local `val` Copy the mutable value into an immutable local: ```kotlin var s: String? = "hi" val local = s if (local != null) { println(local.length) // smart cast works on local } ``` Or more idiomatically with `?:` early return or `?.let`: ```kotlin val local = s ?: return println(local.length) // or s?.let { println(it.length) } ``` ## Key terms - `?.` — **safe call**: calls the member only if the receiver is non-null, otherwise yields `null`. - `!!` — **not-null assertion**: throws `NullPointerException` if the value is null; avoid unless you can prove non-null. - `?:` — **Elvis operator**: provides a fallback when the left side is null. - `let` — scope function that gives the receiver a non-null name (`it`) inside the lambda.

  • Does `?.let { }` count as a smart cast?
    Not exactly. `?.let` only runs the lambda when the receiver is non-null and passes it as `it`, which is a non-null local parameter. It achieves the same effect without relying on smart-cast stability.
  • Is the smart cast blocked even if you never reassign the var?
    Yes. The compiler's analysis is static and conservative; the mere possibility of reassignment (it being a `var`) blocks the cast regardless of actual usage.

A val is a signed contract you can rely on; a var is a sticky note someone could rewrite while you look away.

saying these in an interview costs you the question

  • Claiming smart casts work the same on var and val
  • Saying the compiler inspects runtime values to decide
  • Reaching for !! as the default fix instead of a local val
  • Thinking 'I never reassign it' should make the cast legal

context

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

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

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