Question

Why Kotlin Smart Cast Fails on Mutable Properties: var, Null Safety, and Elegant Fixes

Question

In Kotlin, why does the following code fail to compile even though left is checked for null before it is used?

var left: Node? = null

fun show() {
    if (left != null) {
        queue.add(left) // Error here
    }
}

The compiler reports:

Smart cast to 'Node' is impossible, because 'left' is a mutable property that could have been changed by this time

I understand that left is mutable because it is declared with var, but I am explicitly checking left != null, and left has type Node?. Why can Kotlin not smart-cast it to Node here?

What is the idiomatic and elegant way to fix this?

Short Answer

By the end of this page, you will understand why Kotlin refuses to smart-cast mutable nullable properties, how this relates to null safety and possible state changes, and which common fixes are considered idiomatic in real Kotlin code.

Concept

Kotlin's smart cast feature lets the compiler treat a value as a more specific type after a check.

For example:

val name: String? = "Ada"
if (name != null) {
    println(name.length) // smart-cast to String
}

This works because name is stable inside that scope. Kotlin can prove that it has not changed between the null check and the use.

The problem in your example is that left is a mutable property:

var left: Node? = null

Because it is a var, Kotlin must assume it could change after the check, even if the code looks simple. A mutable property might be changed:

  • by another method
  • by another thread
  • by a custom getter
  • by code elsewhere in the same class

So in this code:

if (left != null) {
    queue.add(left)
}

Kotlin sees two separate reads of left:

  1. one for left != null
  2. another for queue.add(left)

Since left is mutable, the second read is not guaranteed to still be non-null.

That is why Kotlin does not smart-cast it.

This matters in real programming because null safety is only useful if the compiler is strict about values that can change. Kotlin prefers safety over convenience here.

Mental Model

Think of a mutable property like a note on a shared whiteboard.

You look at the whiteboard and see:

  • left = Node(...)

A moment later, before you use it, someone else could erase it and write:

  • left = null

So even though you checked it a second ago, you cannot trust the value unless you copy it somewhere stable.

A local val is like taking a photo of the whiteboard at that moment. That photo does not change.

val currentLeft = left
if (currentLeft != null) {
    queue.add(currentLeft)
}

Now the compiler is happy because currentLeft is stable.

Take Quiz

Syntax and Examples

The most common fixes are based on creating a stable local value or using Kotlin's null-safe operators.

1. Copy to a local val

var left: Node? = null

fun show() {
    val currentLeft = left
    if (currentLeft != null) {
        queue.add(currentLeft)
    }
}

Why it works:

  • currentLeft is a local val
  • it cannot change after assignment
  • Kotlin can safely smart-cast it from Node? to Node

2. Use let

fun show() {
    left?.let { node ->
        queue.add(node)
    }
}

Why it works:

  • left?.let { ... } only runs when left is non-null
  • inside the lambda, node is non-null
  • this is a common idiomatic Kotlin pattern

3. Use the Elvis operator for fallback logic

fun show() {
    val node = left ?: return
    queue.add(node)
}

Why it works:

  • if left is null, the function returns immediately
  • otherwise node is a non-null Node
  • this is a clean example of a guard clause

4. If the property should never change, use val

val left: Node? = null

If the property is not meant to be reassigned, changing var to val may allow smart casting in cases where the property is considered stable.

Example with a custom class

class Node(val value: Int)

val queue = mutableListOf<Node>()
var left: Node? = Node(10)

fun show() {
    val node = left
    if (node != null) {
        queue.add(node)
    }
}

Here, node is a stable snapshot of left. That makes the null check meaningful to the compiler.

Step by Step Execution

Consider this version:

var left: Node? = Node(5)
val queue = mutableListOf<Node>()

fun show() {
    val node = left
    if (node != null) {
        queue.add(node)
    }
}

Step by step:

  1. left starts as Node(5).
  2. show() runs.
  3. val node = left copies the current value of left into a local variable.
  4. node now holds either:
    • a Node, or
    • null
  5. if (node != null) checks the local variable.
  6. Inside the if block, Kotlin knows node cannot be null.
  7. queue.add(node) succeeds because node is treated as Node, not Node?.

Now compare with the original code:

if (left != null) {
    queue.add(left)
}

Kotlin sees:

  1. read left
  2. check for null
  3. read left again
  4. pass it to queue.add(...)

Those are two separate reads of a mutable property, so the compiler refuses to assume they are identical.

Real World Use Cases

This comes up often in Kotlin applications whenever you work with nullable mutable state.

UI state

A screen may hold mutable properties such as:

var selectedUser: User? = null

Before rendering or navigating, you often need a stable local copy.

Tree or graph structures

Your example uses a Node?, which is common in:

  • binary trees
  • parsers
  • linked structures
  • graph traversal

Child references are often nullable, so safe access patterns are important.

API response handling

var cachedResponse: Response? = null

Before reading fields from it, developers usually store a local snapshot or use ?.let.

Background and concurrent code

In multithreaded code, mutable shared properties really can change between check and use. Kotlin's rule helps prevent subtle bugs.

Event-driven apps

In Android, desktop apps, or servers, state may change due to callbacks or lifecycle events. A local val creates a safer point-in-time reference.

Take Quiz

Real Codebase Usage

In real Kotlin codebases, developers usually solve this with a few standard patterns.

Guard clauses

A very common style is to exit early when the value is null:

fun show() {
    val node = left ?: return
    queue.add(node)
}

Benefits:

  • less nesting
  • clear intent
  • easy to read

Safe-call with let

Useful when the action should happen only if the value exists:

fun show() {
    left?.let(queue::add)
}

This is concise and idiomatic.

Local snapshot for stability

When a property may be read multiple times, developers often capture it once:

fun process() {
    val current = left
    if (current != null) {
        use(current)
        log(current)
    }
}

This avoids repeated property reads.

Validation before work

fun send() {
    val session = currentSession ?: error("No active session")
    api.send(session)
}

This pattern is common in service code.

Avoiding !!

In production code, experienced Kotlin developers usually prefer:

  • local val
  • ?: return
  • ?.let

instead of forcing non-null with !!.

Common Mistakes

1. Using !! to silence the compiler

Broken style:

fun show() {
    if (left != null) {
        queue.add(left!!)
    }
}

Why it is risky:

  • !! throws NullPointerException if the value becomes null
  • it bypasses Kotlin's safety checks

Use this only when you are absolutely certain and can justify it.

2. Assuming a null check always guarantees smart cast

Broken assumption:

var value: String? = "hello"
if (value != null) {
    println(value.length) // may fail to compile if value is not stable
}

A null check is not enough by itself. The variable must also be stable.

3. Re-reading mutable properties many times

if (left != null && left.value > 0) {
    queue.add(left)
}

This may lead to multiple compiler errors or unreadable code. Prefer a local snapshot:

val node = left
if (node != null && node.value > 0) {
    queue.add(node)
}

4. Forgetting that custom getters can make properties unstable

val current: Node?
    get() = fetchNode()

Even a val is not always safe for smart cast if it has a custom getter. Stability matters more than just val versus var.

5. Overusing nested if blocks

Less clear:

fun show() {
    if (left != null) {
        val node = left
        if (node != null) {
            queue.add(node)
        }
    }
}

Better:

fun show() {
    val node = left ?: return
    queue.add(node)
}

Comparisons

ApproachExampleSafeIdiomaticNotes
Null check on mutable propertyif (left != null) queue.add(left)No for smart castNoFails because left can change
Local val snapshotval node = left; if (node != null) queue.add(node)YesYesClear and reliable
Safe call with letleft?.let(queue::add)YesYesShort and expressive
Elvis with early returnval node = left ?: returnYesYesGreat for guard clauses
Force unwrapqueue.add(left!!)Potentially unsafeUsually noCan throw at runtime

Smart cast on val vs var

Property typeSmart-cast friendly?Why
Local valUsually yesStable in scope
Local varSometimesOnly if compiler can prove no changes
Class valSometimesDepends on visibility and getter behavior
Class varUsually noMutable property may change

if vs ?.let

StyleBest when
if (node != null)You want explicit flow and multiple statements
?.let { ... }You want concise null-only execution
?: returnYou want early exit and flatter code
Take Quiz

Cheat Sheet

// Problem
var left: Node? = null
if (left != null) {
    queue.add(left) // smart cast fails
}

Why it fails

  • left is a mutable property (var)
  • Kotlin sees two reads of left
  • it could change between the null check and usage

Best fixes

val node = left
if (node != null) {
    queue.add(node)
}
left?.let { queue.add(it) }
val node = left ?: return
queue.add(node)

Prefer

  • local val for stable snapshot
  • ?.let for concise null handling
  • ?: return for guard clauses

Avoid

queue.add(left!!)

unless you truly want to risk a runtime exception.

Rule of thumb

A null check enables smart cast only when the value is stable.

FAQ

Why does Kotlin not trust left after if (left != null)?

Because left is a mutable property. Kotlin cannot guarantee it still has the same value when it is read again.

Does this only happen with var?

Mostly, yes. Mutable properties are the common case. But some val properties also cannot be smart-cast if they have custom getters or are otherwise not stable.

What is the most idiomatic fix in Kotlin?

Usually one of these:

  • val node = left ?: return
  • left?.let { ... }
  • copy to a local val

Is left!! a good fix?

It compiles, but it is usually not the best fix. It can throw NullPointerException at runtime.

Why does a local variable work better?

A local val is stable. Once assigned, it cannot change, so Kotlin can smart-cast it safely.

Is this related to multithreading?

Yes, that is one reason. But even without threads, Kotlin still applies the rule because mutable properties may change through other code paths.

Can I just change var to val?

If the property should not be reassigned, yes. But if it truly needs to be mutable, use a local snapshot or a null-safe pattern instead.

Take Quiz

Mini Project

Description

Build a small Kotlin example that processes an optional current user before adding it to a list of active users. This demonstrates how to safely work with a mutable nullable property using a local snapshot and Kotlin null-handling patterns.

Goal

Create a function that safely adds a nullable mutable property to a collection only when it is not null.

Requirements

  1. Create a User data class with a name property.
  2. Store the current user in a mutable nullable property.
  3. Write a function that adds the current user to a list only if it exists.
  4. Avoid using !!.
  5. Use either a local val, ?.let, or an early return pattern.
Take Quiz

Keep learning

Related questions

View more