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:
- one for
left != null
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.
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:
currentLeftis a localval- it cannot change after assignment
- Kotlin can safely smart-cast it from
Node?toNode
2. Use let
fun show() {
left?.let { node ->
queue.add(node)
}
}
Why it works:
left?.let { ... }only runs whenleftis non-null- inside the lambda,
nodeis non-null
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:
leftstarts asNode(5).show()runs.val node = leftcopies the current value ofleftinto a local variable.nodenow holds either:- a
Node, or null
- a
if (node != null)checks the local variable.- Inside the
ifblock, Kotlin knowsnodecannot benull. queue.add(node)succeeds because is treated as , not .
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.
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
Common Mistakes
1. Using !! to silence the compiler
Broken style:
fun show() {
if (left != null) {
queue.add(left!!)
}
}
Why it is risky:
!!throwsNullPointerExceptionif 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:
Comparisons
| Approach | Example | Safe | Idiomatic | Notes |
|---|---|---|---|---|
| Null check on mutable property | if (left != null) queue.add(left) | No for smart cast | No | Fails because left can change |
Local val snapshot | val node = left; if (node != null) queue.add(node) | Yes | Yes | Clear and reliable |
Safe call with let | left?.let(queue::add) | Yes | Yes | Short and expressive |
| Elvis with early return |
Cheat Sheet
// Problem
var left: Node? = null
if (left != null) {
queue.add(left) // smart cast fails
}
Why it fails
leftis 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
valfor stable snapshot ?.letfor concise null handling?: returnfor guard clauses
Avoid
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 ?: returnleft?.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.
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
- Create a
Userdata class with anameproperty. - Store the current user in a mutable nullable property.
- Write a function that adds the current user to a list only if it exists.
- Avoid using
!!. - Use either a local
val,?.let, or an early return pattern.
Keep learning
Related questions
Android AlarmManager Example: Scheduling Tasks with AlarmManager
Learn how to use Android AlarmManager to schedule tasks, set alarms, and handle broadcasts with a simple beginner example.
Can You Extend a Data Class in Kotlin? Inheritance, Limits, and Better Alternatives
Learn why Kotlin data classes cannot be extended, what causes the component function clash, and which alternatives to use instead.
Difference Between List and Array in Kotlin
Learn the difference between List and Array in Kotlin, including mutability, size, APIs, performance, and when to use each one.