skip to content

What are the failure modes and edge cases of `isInitialized` — type restrictions, concurrency, and what it does NOT guarantee?

level: seniorimportance: should knowfreq 30%

answer

  1. No val / primitive / nullable (lateinit limits)
  2. Means 'assigned once', not 'valid/fresh'
  3. Cannot un-initialize: stays true forever
  4. Check-then-act race under concurrency
  5. Concurrency-safe alt: by lazy / AtomicReference / StateFlow

basics

~20 s

It only works on lateinit (no val, primitives, or nullable types), only inside the declaring class, and only tells you 'has been assigned' — not 'currently valid'. Under threads, true now doesn't guarantee the value is still usable later.

solid answer

~40 s

`isInitialized` has sharp edges. It compiles only against a bound `KProperty0` of a `lateinit` property — never a `val`, a primitive-typed property, or a nullable type, since `lateinit` itself forbids those. It's confined to the declaring class (and enclosing nested/inner scopes) because it reads the private backing field. Semantically it answers only 'was a value ever assigned' — not whether the value is logically valid or non-stale. In concurrent code, `if (isInitialized) read()` is a **check-then-act** race: another thread could be assigning concurrently, and `lateinit` assignment isn't atomic-with-your-read unless you add synchronization, `@Volatile` won't help (lateinit doesn't support it), so guard with a lock or prefer `by lazy` (thread-safe) / `AtomicReference`. Also note: once assigned, you can't reset a `lateinit` back to 'uninitialized'; `isInitialized` will stay `true` for the object's lifetime.

code

kotlin · 8 lines
kotlin
class Cache {
    private lateinit var data: List<Int>
    private val lock = Any()
    fun init(v: List<Int>) = synchronized(lock) { data = v }
    fun safeRead(): List<Int>? = synchronized(lock) {
        if (this::data.isInitialized) data else null
    }
}

go deeper

for a junior

Knows it can't be used on val/primitive and that it means 'assigned'.

for a middle

Lists type/scope restrictions and that it only signals assignment, not validity.

for a senior

Identifies the check-then-act concurrency race, the no-reset semantics, and names lazy/AtomicReference/StateFlow as safe alternatives.

for a principal

Reasons about safe publication and happens-before, and designs shared late state with thread-safe holders rather than lateinit guards.

## Type and scope restrictions - **`lateinit` only.** `isInitialized` resolves only on a bound reference to a `lateinit` property. A `val`, a non-lateinit `var`, a primitive (`Int`, `Boolean`, …), or a **nullable** property won't compile — because `lateinit` itself cannot be applied to `val`, primitives, or nullable types. - **Declaring-class scope.** It reads the private backing field, so it's usable inside the declaring class and its nested/inner classes (`this@Outer::prop`), not from unrelated callers. ## What it does and does NOT mean - It means: **a value has been assigned at least once.** - It does **not** mean the value is logically valid, fresh, or in a usable state — only that the field is no longer the uninitialized sentinel. - **No reset.** There is no API to un-initialize a `lateinit`. After the first assignment, `isInitialized` returns `true` forever for that instance; you cannot make it `false` again. ## Concurrency: check-then-act race ```kotlin class Cache { private lateinit var data: List<Int> fun init(v: List<Int>) { data = v } fun safeRead(): List<Int>? = if (this::data.isInitialized) data else null // RACE if init() runs on another thread } ``` - `isInitialized` then `data` is two operations. Between them another thread could be assigning. The read might see a torn/partial publication without proper synchronization. - `lateinit` does **not** support `@Volatile`, and assignment carries no built-in happens-before guarantee for other threads' reads. - Fixes: wrap check+read in a lock; or use `by lazy { }` (synchronized by default, gives safe publication); or `AtomicReference<T?>` / `StateFlow<T?>` when you need observable, thread-safe late values. ## Reflection cost (minor) `this::prop` allocates a bound `KProperty0` and reflective metadata is involved. It's cheap and fine for occasional guards, but doing it in a tight hot loop is wasteful — hoist or restructure. ## Practical guidance - Use `isInitialized` for single-threaded lifecycle guards (Android views, test fixtures, `@PostConstruct` ordering). - For shared mutable late state, reach for `lazy`, `AtomicReference`, or a `StateFlow`/`MutableStateFlow` instead, where readiness and value are published together safely.

  • Can you reset a lateinit so isInitialized becomes false again?
    No. There's no un-initialize API; once assigned it stays initialized for that instance's lifetime.
  • Is `if (isInitialized) read()` thread-safe across threads?
    No — it's a check-then-act race. Use a lock, by lazy, or AtomicReference/StateFlow for shared late state.
  • Why can't you put @Volatile on a lateinit var to make the read safe?
    lateinit doesn't support @Volatile; you must synchronize externally or use a thread-safe holder instead.

saying these in an interview costs you the question

  • Treating isInitialized true as proof the value is valid/current
  • Assuming check-then-act with isInitialized is thread-safe
  • Claiming you can reset a lateinit to uninitialized
  • Thinking lateinit/isInitialized works on Int/Boolean or nullable types
  • Calling reflective isInitialized in a hot loop without concern

context