{"id":754,"date":"2026-10-05T13:50:00","date_gmt":"2026-10-05T11:50:00","guid":{"rendered":"https:\/\/grindloop.io\/blog\/?p=754"},"modified":"2026-10-05T14:06:04","modified_gmt":"2026-10-05T12:06:04","slug":"non-null-kotlin-property-is-null","status":"publish","type":"post","link":"https:\/\/grindloop.ai\/blog\/non-null-kotlin-property-is-null\/","title":{"rendered":"Why a Non-Null Kotlin Property Is Null When the Base Class Init Calls an Open Function"},"content":{"rendered":"<p>A non-null Kotlin property is null when the base class <code>init<\/code> 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&#8217;s fields have been assigned yet. They still hold the JVM default. That is null for objects and 0 for numbers. That includes <code>private val<\/code> constructor parameters. The type says <code>String<\/code>. 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&#8217;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&#8217;s own <code>init<\/code>, after its properties, or from an explicit lifecycle call.<\/p>\n\n<h2>A BaseViewModel that loads data from init crashes its subclass<\/h2>\n\n<p>The running example is a pattern that shows up in Android codebases and in bug-squash snippets. A <code>BaseViewModel<\/code> wants every screen to load on creation, so it calls an abstract hook from <code>init<\/code>. <code>ProfileViewModel<\/code> implements the hook with its repository and a user ID. To compile it outside Android, <code>ViewModel<\/code> here is a plain stand-in class. Nothing in the bug depends on <code>androidx.lifecycle<\/code>.<\/p>\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"kotlin\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">open class ViewModel \/\/ stand-in for androidx.lifecycle.ViewModel\n\ninterface ProfileRepository { fun load(userId: String): String }\nclass FakeRepo : ProfileRepository { override fun load(userId: String) = \"Profile($userId)\" }\n\nabstract class BaseViewModel : ViewModel() {\n    init {\n        loadInitialData()\n    }\n    protected abstract fun loadInitialData()\n}\n\nclass ProfileViewModel(\n    private val repository: ProfileRepository,\n) : BaseViewModel() {\n    private val userId = \"u-42\"\n    private val history = mutableListOf&lt;String&gt;()\n\n    override fun loadInitialData() {\n        println(\"userId=$userId history=$history repository=$repository\")\n        history += repository.load(userId)\n    }\n}\n\nfun main() {\n    ProfileViewModel(FakeRepo())\n}<\/pre>\n\n\n<p>Compiled with <code>kotlinc<\/code> 2.4.20 and run on the JVM, it prints one line and then crashes:<\/p>\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">userId=null history=null repository=null\nException in thread \"main\" java.lang.NullPointerException: Cannot invoke \"ProfileRepository.load(String)\" because \"this.repository\" is null\n\tat ProfileViewModel.loadInitialData(Main.kt:21)\n\tat BaseViewModel.&lt;init&gt;(Main.kt:8)\n\t...<\/pre>\n\n\n<p>All three properties are non-null types. All three are null. The <code>repository<\/code> case is the confusing one. It was passed in as an argument. That argument wasn&#8217;t null. The build printed no warning for either class. The second stack frame is the clue. The override ran inside <code>BaseViewModel.&lt;init&gt;<\/code>, the base class constructor.<\/p>\n\n<h2>A non-null Kotlin property is null because the base init runs first<\/h2>\n\n<p>The <a href=\"https:\/\/kotlinlang.org\/docs\/inheritance.html\" target=\"_blank\" rel=\"noopener\">Kotlin inheritance docs<\/a> 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&#8217;s initialization logic runs after it. The docs then warn about this exact case. The derived class&#8217;s properties aren&#8217;t initialized while the base constructor runs. The docs say using them from base class initialization &#8220;may lead to incorrect behavior or a runtime failure.&#8221; That holds for direct reads and for reads through an overridden <code>open<\/code> member.<\/p>\n\n<p>Walk <code>ProfileViewModel(FakeRepo())<\/code> through that order:<\/p>\n\n<ol>\n<li>The JVM allocates the object. Every field starts at its default. References are null. An <code>Int<\/code> would be 0.<\/li>\n<li><code>BaseViewModel<\/code>&#8216;s <code>init<\/code> runs and calls <code>loadInitialData()<\/code>. The call dispatches to the override in <code>ProfileViewModel<\/code>, because the object already is a <code>ProfileViewModel<\/code>.<\/li>\n<li>The override reads <code>userId<\/code>, <code>history<\/code> and <code>repository<\/code>. Nothing has assigned them yet, so it sees three nulls and crashes on <code>repository.load()<\/code>.<\/li>\n<li>Had it survived, <code>ProfileViewModel<\/code>&#8216;s own initialization would run next. That&#8217;s where <code>repository<\/code> gets stored from the argument and <code>userId<\/code> gets <code>\"u-42\"<\/code>.<\/li>\n<\/ol>\n\n<p>Step 4 explains the constructor parameter. A <code>private val<\/code> in the primary constructor is a property. Its field is assigned in the subclass&#8217;s initialization, like any other property. The argument exists in step 2. The field just doesn&#8217;t hold it yet. A string literal doesn&#8217;t help either. <code>\"u-42\"<\/code> is a constant. Still, <code>userId<\/code> isn&#8217;t a <code>const val<\/code>. Its field gets the value in step 4, like the rest.<\/p>\n\n<h2>The compiler checks a direct read but not a read through a call<\/h2>\n\n<p>Kotlin does guard part of this. If an <code>init<\/code> block reads a property above its declaration, the build fails. With 2.4.20, the error is &#8220;variable &#8216;userId&#8217; must be initialized.&#8221; If the same read moves into a function, the check stops:<\/p>\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"kotlin\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">class Early {\n    init { show() }\n    private val userId = \"u-42\"\n    private fun show() = println(\"init sees userId=$userId\")\n}\n\nfun main() { Early() }<\/pre>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">init sees userId=null<\/pre>\n\n\n<p>That compiles with no warning. There&#8217;s no inheritance in it at all. The check works inside one initializer, line by line. It doesn&#8217;t follow calls. An override in a subclass is even further out of its reach. When <code>BaseViewModel<\/code> compiles, the compiler doesn&#8217;t know which subclasses will exist.<\/p>\n\n<p>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&#8217;s <code>repository.load()<\/code>. A null <code>userId<\/code> passed to a Java method could travel further first. The <a href=\"https:\/\/kotlinlang.org\/docs\/null-safety.html\" target=\"_blank\" rel=\"noopener\">Kotlin null safety docs<\/a> 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.<\/p>\n\n<h2>Start the work after construction, from code that knows the order<\/h2>\n\n<p>The base class can&#8217;t make its subclasses safe to call during its own <code>init<\/code>. So it shouldn&#8217;t call them there. The inheritance docs give the same advice. Avoid <code>open<\/code> members in constructors, property initializers and <code>init<\/code> blocks. For the running example, the smallest fix moves the call into the final subclass:<\/p>\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"kotlin\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">abstract class BaseViewModel : ViewModel()\n\nclass ProfileViewModel(\n    private val repository: ProfileRepository,\n) : BaseViewModel() {\n    private val userId = \"u-42\"\n    private val history = mutableListOf&lt;String&gt;()\n\n    init {\n        loadInitialData()\n    }\n\n    private fun loadInitialData() {\n        history += repository.load(userId)\n        println(\"history=$history\")\n    }\n}\n\nfun main() {\n    ProfileViewModel(FakeRepo())\n}<\/pre>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">history=[Profile(u-42)]<\/pre>\n\n\n<p>Three details make this version safe.<\/p>\n\n<ul>\n<li><code>ProfileViewModel<\/code> is final, so no class below it can run code before its fields are set.<\/li>\n<li><code>loadInitialData()<\/code> is <code>private<\/code>, so no subclass can override it.<\/li>\n<li>The <code>init<\/code> block sits below the properties. The <a href=\"https:\/\/kotlinlang.org\/docs\/classes.html\" target=\"_blank\" rel=\"noopener\">Kotlin classes docs<\/a> cover the order. <code>init<\/code> blocks and property initializers run in the order they appear in the class body. The <code>Early<\/code> example shows what happens when the call comes first.<\/li>\n<\/ul>\n\n<p>Two other shapes work when the base class needs to drive the flow. The first passes data up instead of calling down. <code>BaseScreen(screenName: String)<\/code> can use its own constructor parameter safely, because the base class assigns it before its <code>init<\/code> runs. The second makes the start explicit. The base class exposes a <code>start()<\/code> function. The owner calls it after construction. The base can then call open hooks from <code>start()<\/code>, because every field is set by then.<\/p>\n\n<h2>How to explain the crash in a bug-squash round<\/h2>\n\n<p>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 <a href=\"https:\/\/grindloop.ai\/blog\/android-debugging-interview-second-bug\/\">how the Android debugging interview works<\/a> covers how that round is scored. This is the explanation to give for this bug.<\/p>\n\n<ol>\n<li>Name the order. The base class&#8217;s <code>init<\/code> runs before the subclass&#8217;s fields are assigned. The overridden <code>loadInitialData()<\/code> runs inside that window. So it sees JVM defaults. That includes the constructor parameter.<\/li>\n<li>Explain why the types didn&#8217;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.<\/li>\n<li>Give the fix. The base constructor stops calling overridable members. The final subclass calls a private function from an <code>init<\/code> placed after its properties. Name constructor parameters or an explicit <code>start()<\/code> as the options when the base must control the flow.<\/li>\n<li>Mention the property-order trap inside one class. An <code>init<\/code> above a property declaration, calling a function that reads it, fails the same way.<\/li>\n<\/ol>\n\n<p>Step 2 shows you know where Kotlin&#8217;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&#8217;s <code>equals()<\/code> skips properties declared in its parent. See <a href=\"https:\/\/grindloop.ai\/blog\/kotlin-data-class-equals-ignores-parent-class\/\">why a data class&#8217;s equals() ignores the parent class<\/a>.<\/p>\n\n<h2>Four fixes that compile and still leave the bug<\/h2>\n\n<p>Each of these came out of the same <code>kotlinc<\/code> 2.4.20 run, applied to the <code>ProfileViewModel<\/code> hook.<\/p>\n\n<ul>\n<li>A safe call, <code>repository?.load(userId)<\/code>, stops the crash. The compiler warns about an &#8220;unnecessary safe call on a non-null receiver.&#8221; At runtime, the call is skipped. The screen never loads. Nothing reports why.<\/li>\n<li>Making the property <code>by lazy<\/code> moves the crash. A delegated property stores its delegate in a field too. That field isn&#8217;t assigned yet either. Reading <code>userId<\/code> through <code>by lazy<\/code> from the hook throws an NPE: <code>Cannot invoke \"kotlin.Lazy.getValue()\"<\/code>.<\/li>\n<li>Assigning the property inside the hook loses the value. Suppose the hook sets <code>history = mutableListOf(\"opened\")<\/code>. The property itself is declared with <code>= mutableListOf()<\/code>. The hook runs first. Then the subclass&#8217;s initializer runs and replaces the list. After construction, <code>history<\/code> is empty again.<\/li>\n<li>A <code>lateinit<\/code> property assigned in the hook does survive, because it has no initializer to overwrite it. But reading it in the hook before assignment throws <code>UninitializedPropertyAccessException<\/code>, as the <a href=\"https:\/\/kotlinlang.org\/docs\/properties.html\" target=\"_blank\" rel=\"noopener\">Kotlin properties docs<\/a> describe. Every other property is still null in the hook. It works only while the hook touches nothing else.<\/li>\n<\/ul>\n\n<p>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 <code>init<\/code> fixes all of them at once. A null can also survive a null check in Compose. See <a href=\"https:\/\/grindloop.ai\/blog\/compose-null-check-crash\/\">the null check that didn&#8217;t save you<\/a>.<\/p>","protected":false},"excerpt":{"rendered":"<p>A non-null Kotlin property is null when a base class init calls an open function. Why the subclass fields are unset, why it compiles, and the fix.<\/p>\n","protected":false},"author":2,"featured_media":756,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Why a Non-Null Kotlin Property Is Null When the Base Class Init Calls an Open Function","rank_math_description":"A non-null Kotlin property is null when a base class init calls an open function. Why the subclass fields are unset, why it compiles, and the fix.","rank_math_focus_keyword":"non-null kotlin property is null","footnotes":""},"categories":[9,4],"tags":[15,16,12,98,67,45],"class_list":["post-754","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-bug-squash","category-oop-solid","tag-android","tag-interview-prep","tag-kotlin","tag-null-safety","tag-oop-solid","tag-technical-interview"],"_links":{"self":[{"href":"https:\/\/grindloop.ai\/blog\/wp-json\/wp\/v2\/posts\/754","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/grindloop.ai\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/grindloop.ai\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/grindloop.ai\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/grindloop.ai\/blog\/wp-json\/wp\/v2\/comments?post=754"}],"version-history":[{"count":1,"href":"https:\/\/grindloop.ai\/blog\/wp-json\/wp\/v2\/posts\/754\/revisions"}],"predecessor-version":[{"id":755,"href":"https:\/\/grindloop.ai\/blog\/wp-json\/wp\/v2\/posts\/754\/revisions\/755"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/grindloop.ai\/blog\/wp-json\/wp\/v2\/media\/756"}],"wp:attachment":[{"href":"https:\/\/grindloop.ai\/blog\/wp-json\/wp\/v2\/media?parent=754"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/grindloop.ai\/blog\/wp-json\/wp\/v2\/categories?post=754"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/grindloop.ai\/blog\/wp-json\/wp\/v2\/tags?post=754"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}