skip to content

Give cases where Kotlin refuses to smart-cast after an `is`/null check, and how you work around each.

level: middleimportance: must knowfreq 70%

answer

  1. stable value required
  2. var props, custom getter, open, other module = unstable
  3. snapshot into local val
  4. ?.let captures a stable receiver
  5. delegated (by) props not smart-cast

basics

~20 s

Kotlin won't smart-cast when the value could change between check and use: mutable var properties, properties with custom getters, open properties, and values from other modules. Copy the value into a local val (or use ?.let) and check that instead.

solid answer

~50 s

Smart casts require a **stable** value. The compiler refuses to narrow when it can't guarantee the runtime type still holds at the point of use: - **`var` properties** (member or top-level): could be reassigned by another thread between check and use. - **Properties with a custom getter or `open` properties**: each access may return a different value, so checking once proves nothing. - **Delegated properties (`by`)**: value comes from the delegate's getter. - **Properties declared in another module**: the compiler can't see the backing field / mutability guarantees. - **`var` locals mutated by a closure** that could run between check and use. Workarounds: assign to a **local `val`** and check that (`val s = obj.name; if (s is String) ...`), use **`?.let { }`** which captures the receiver, or use the `!!` / explicit narrowing only when truly safe. Local `val`s and `val` properties with backing fields are always smart-castable.

code

kotlin · 8 lines
kotlin
class Node { var next: Any? = null }
fun handle(n: Node) {
    // n.next is a var property -> not smart-castable
    val next = n.next            // snapshot
    if (next is String) {
        println(next.uppercase()) // OK now
    }
}

go deeper

for a junior

Recognizes the 'smart cast impossible' error and knows copying into a local val fixes it.

for a middle

Lists the unstable categories (var, custom getter, open, other module, delegated) and the snapshot/let workarounds.

for a senior

Explains the thread-safety and getter-reentrancy reasoning behind stability and chooses idiomatic fixes per case.

for a principal

Designs APIs/properties to maximize smart-castability and weighs immutability vs. defensive copying across module boundaries.

## Why smart casts can fail A smart cast is only sound if the value the compiler checked is the *same* value you later use, with the *same* runtime type. Kotlin calls such values **stable**. When stability can't be proven, the compiler emits `Smart cast to 'T' is impossible, because '...' is a ...` and you must cast explicitly or restructure. ## The unstable cases ### 1. Mutable `var` properties ```kotlin class Box { var item: Any? = null } fun use(b: Box) { if (b.item is String) { // ERROR: b.item is a var property; another thread could reassign it // println(b.item.length) } } ``` ### 2. Custom getters / `open` properties ```kotlin val now: Any get() = System.currentTimeMillis() // returns a fresh value each call // if (now is Long) { now.toString() } // not stable — getter re-runs ``` An `open val` is also unstable because a subclass could override it with a custom getter. ### 3. Properties from another module Even a `val` property declared in a different compilation module isn't smart-cast by default, because the compiler treats it conservatively. ### 4. Delegated properties ```kotlin val lazyVal: Any by lazy { compute() } // value is produced through the delegate's getValue — not smart-castable ``` ## Workarounds ### Capture into a local `val` ```kotlin fun use(b: Box) { val item = b.item // stable local val if (item is String) println(item.length) // OK } ``` ### Use `?.let` ```kotlin (b.item as? String)?.let { println(it.length) } // or, staying within is-checks, capture first then let b.item?.let { value -> if (value is String) println(value.length) } ``` `let` captures the receiver into the lambda parameter, which is a stable `val`. ## What is always stable - local `val`s - `val` properties with a backing field in the **same module** that are not `open` and have no custom getter - the implicit receiver inside `let`/`run`/`apply` lambdas ## Key mental model If the value can be re-read and return something different (var, getter, delegate, cross-module), Kotlin won't smart-cast. Snapshot it into a `val` and the problem disappears.

  • Why does `?.let { }` succeed where a direct `if (prop is T)` fails?
    `let` evaluates the receiver once and binds it to a stable lambda parameter (`it`), so subsequent uses reference that fixed value rather than re-reading the unstable property.
  • Is a `val` property always smart-castable?
    No — not if it has a custom getter, is `open`, is delegated, or lives in another module. Plain `val`s with a backing field in the same module are.

saying these in an interview costs you the question

  • Claiming any `val` is always smart-castable
  • Saying smart casts fail randomly with no rule
  • Not knowing the 'snapshot into local val' fix
  • Believing custom getters are safe to smart-cast

context