Why a Non-Null Kotlin Property Is Null When the Base Class Init Calls an Open Function

A non-null Kotlin property is null when the base class init calls an overridable function. The subclass overrides that function and reads its own properties. Kotlin runs the base class initialization first. So at that moment, none of the subclass’s fields have been assigned yet. They still hold the JVM default. That is null for objects and 0 for numbers. That includes private val constructor parameters. The type says String. The field holds null. A second cause keeps the bug quiet. The compiler rejects a direct read of an unassigned property in an initializer. It doesn’t follow that read through a function call. It also emits no null check when a class reads its own field. So the null travels until something dereferences it. The fix is a rule for the base class. Its constructor must never call a member a subclass can override. Start the work from the subclass’s own init, after its properties, or from an explicit lifecycle call.

A BaseViewModel that loads data from init crashes its subclass

The running example is a pattern that shows up in Android codebases and in bug-squash snippets. A BaseViewModel wants every screen to load on creation, so it calls an abstract hook from init. ProfileViewModel implements the hook with its repository and a user ID. To compile it outside Android, ViewModel here is a plain stand-in class. Nothing in the bug depends on androidx.lifecycle.

open class ViewModel // stand-in for androidx.lifecycle.ViewModel

interface ProfileRepository { fun load(userId: String): String }
class FakeRepo : ProfileRepository { override fun load(userId: String) = "Profile($userId)" }

abstract class BaseViewModel : ViewModel() {
    init {
        loadInitialData()
    }
    protected abstract fun loadInitialData()
}

class ProfileViewModel(
    private val repository: ProfileRepository,
) : BaseViewModel() {
    private val userId = "u-42"
    private val history = mutableListOf<String>()

    override fun loadInitialData() {
        println("userId=$userId history=$history repository=$repository")
        history += repository.load(userId)
    }
}

fun main() {
    ProfileViewModel(FakeRepo())
}

Compiled with kotlinc 2.4.20 and run on the JVM, it prints one line and then crashes:

userId=null history=null repository=null
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "ProfileRepository.load(String)" because "this.repository" is null
	at ProfileViewModel.loadInitialData(Main.kt:21)
	at BaseViewModel.<init>(Main.kt:8)
	...

All three properties are non-null types. All three are null. The repository case is the confusing one. It was passed in as an argument. That argument wasn’t null. The build printed no warning for either class. The second stack frame is the clue. The override ran inside BaseViewModel.<init>, the base class constructor.

A non-null Kotlin property is null because the base init runs first

The Kotlin inheritance docs spell out the order. Base class initialization happens first. Only the evaluation of the arguments to the base class constructor comes before it. The derived class’s initialization logic runs after it. The docs then warn about this exact case. The derived class’s properties aren’t initialized while the base constructor runs. The docs say using them from base class initialization “may lead to incorrect behavior or a runtime failure.” That holds for direct reads and for reads through an overridden open member.

Walk ProfileViewModel(FakeRepo()) through that order:

  1. The JVM allocates the object. Every field starts at its default. References are null. An Int would be 0.
  2. BaseViewModel‘s init runs and calls loadInitialData(). The call dispatches to the override in ProfileViewModel, because the object already is a ProfileViewModel.
  3. The override reads userId, history and repository. Nothing has assigned them yet, so it sees three nulls and crashes on repository.load().
  4. Had it survived, ProfileViewModel‘s own initialization would run next. That’s where repository gets stored from the argument and userId gets "u-42".

Step 4 explains the constructor parameter. A private val in the primary constructor is a property. Its field is assigned in the subclass’s initialization, like any other property. The argument exists in step 2. The field just doesn’t hold it yet. A string literal doesn’t help either. "u-42" is a constant. Still, userId isn’t a const val. Its field gets the value in step 4, like the rest.

The compiler checks a direct read but not a read through a call

Kotlin does guard part of this. If an init block reads a property above its declaration, the build fails. With 2.4.20, the error is “variable ‘userId’ must be initialized.” If the same read moves into a function, the check stops:

class Early {
    init { show() }
    private val userId = "u-42"
    private fun show() = println("init sees userId=$userId")
}

fun main() { Early() }
init sees userId=null

That compiles with no warning. There’s no inheritance in it at all. The check works inside one initializer, line by line. It doesn’t follow calls. An override in a subclass is even further out of its reach. When BaseViewModel compiles, the compiler doesn’t know which subclasses will exist.

Once the null is in the field, nothing stops it at the read. Kotlin trusts its own non-null types when a class reads its own field. So it adds no check there. The null moves on until a call on it fails. In the example, that’s repository.load(). A null userId passed to a Java method could travel further first. The Kotlin null safety docs list this among the few ways to get an NPE in Kotlin. They call it data inconsistency during initialization. One named case is a superclass constructor that calls an open member whose override uses uninitialized state.

Start the work after construction, from code that knows the order

The base class can’t make its subclasses safe to call during its own init. So it shouldn’t call them there. The inheritance docs give the same advice. Avoid open members in constructors, property initializers and init blocks. For the running example, the smallest fix moves the call into the final subclass:

abstract class BaseViewModel : ViewModel()

class ProfileViewModel(
    private val repository: ProfileRepository,
) : BaseViewModel() {
    private val userId = "u-42"
    private val history = mutableListOf<String>()

    init {
        loadInitialData()
    }

    private fun loadInitialData() {
        history += repository.load(userId)
        println("history=$history")
    }
}

fun main() {
    ProfileViewModel(FakeRepo())
}
history=[Profile(u-42)]

Three details make this version safe.

  • ProfileViewModel is final, so no class below it can run code before its fields are set.
  • loadInitialData() is private, so no subclass can override it.
  • The init block sits below the properties. The Kotlin classes docs cover the order. init blocks and property initializers run in the order they appear in the class body. The Early example shows what happens when the call comes first.

Two other shapes work when the base class needs to drive the flow. The first passes data up instead of calling down. BaseScreen(screenName: String) can use its own constructor parameter safely, because the base class assigns it before its init runs. The second makes the start explicit. The base class exposes a start() function. The owner calls it after construction. The base can then call open hooks from start(), because every field is set by then.

How to explain the crash in a bug-squash round

In a bug-squash round, this snippet would come with the stack trace above and one question. Why is a non-null property null? The post on how the Android debugging interview works covers how that round is scored. This is the explanation to give for this bug.

  1. Name the order. The base class’s init runs before the subclass’s fields are assigned. The overridden loadInitialData() runs inside that window. So it sees JVM defaults. That includes the constructor parameter.
  2. Explain why the types didn’t catch it. The compiler checks direct reads inside one initializer, not reads through calls or overrides. Kotlin adds no null check when a class reads its own field. The Kotlin docs list this case as one of the few sources of an NPE.
  3. Give the fix. The base constructor stops calling overridable members. The final subclass calls a private function from an init placed after its properties. Name constructor parameters or an explicit start() as the options when the base must control the flow.
  4. Mention the property-order trap inside one class. An init above a property declaration, calling a function that reads it, fails the same way.

Step 2 shows you know where Kotlin’s null safety stops. A strong candidate can say both why it compiled and why it crashed. The same class can hide another inheritance trap. A data class’s equals() skips properties declared in its parent. See why a data class’s equals() ignores the parent class.

Four fixes that compile and still leave the bug

Each of these came out of the same kotlinc 2.4.20 run, applied to the ProfileViewModel hook.

  • A safe call, repository?.load(userId), stops the crash. The compiler warns about an “unnecessary safe call on a non-null receiver.” At runtime, the call is skipped. The screen never loads. Nothing reports why.
  • Making the property by lazy moves the crash. A delegated property stores its delegate in a field too. That field isn’t assigned yet either. Reading userId through by lazy from the hook throws an NPE: Cannot invoke "kotlin.Lazy.getValue()".
  • Assigning the property inside the hook loses the value. Suppose the hook sets history = mutableListOf("opened"). The property itself is declared with = mutableListOf(). The hook runs first. Then the subclass’s initializer runs and replaces the list. After construction, history is empty again.
  • A lateinit property assigned in the hook does survive, because it has no initializer to overwrite it. But reading it in the hook before assignment throws UninitializedPropertyAccessException, as the Kotlin properties docs describe. Every other property is still null in the hook. It works only while the hook touches nothing else.

All four treat the symptom in one property. The order stays broken for every property the hook might read next. Moving the call out of the base init fixes all of them at once. A null can also survive a null check in Compose. See the null check that didn’t save you.

Leave a Comment