skip to content

What exactly happens when you read a `lateinit var` before assigning it, and how does this differ from a nullable property?

level: middleimportance: should knowfreq 55%

answer

  1. Early read -> UninitializedPropertyAccessException
  2. Subclass of RuntimeException, message names the prop
  3. Fail-fast at access site, not silent default
  4. Nullable returns null instead of throwing
  5. lateinit = always present; nullable = maybe absent

basics

~20 s

Reading a lateinit var before it's set throws an UninitializedPropertyAccessException with a message naming the property. A nullable property instead returns null, which you can handle with ?. or ?: — it never throws on read.

solid answer

~40 s

On early read of an unassigned `lateinit` property, the compiler-generated getter detects the internal null sentinel and throws `UninitializedPropertyAccessException` (a subclass of `RuntimeException`), with a message like 'lateinit property X has not been initialized'. This is fail-fast: it surfaces the bug at the exact access site rather than letting a default value silently flow through. A nullable property (`Type?`) behaves completely differently — it legitimately holds `null` and returns it; you handle absence with `?.`, `?:`, `let`, or `!!`. The trade-off: `lateinit` keeps non-null types and crashes loudly if your setup ordering is wrong; nullable models genuine optionality but forces null handling everywhere. `lateinit` is for 'definitely present after setup', nullable is for 'may legitimately be absent'.

code

kotlin · 7 lines
kotlin
class A { lateinit var s: String }
class B { var s: String? = null }

fun demo() {
    // A().s.length            // throws UninitializedPropertyAccessException
    println(B().s?.length)     // prints null, no throw
}

go deeper

for a junior

Knows reading before assignment throws an exception rather than returning null.

for a middle

Names UninitializedPropertyAccessException and contrasts it cleanly with nullable behavior.

for a senior

Frames the throw as intentional fail-fast and explains when lateinit vs nullable is the right contract.

for a principal

Argues that swapping a runtime guarantee for a compile-time one is a deliberate design trade and avoids exception-driven control flow.

## The exception ```kotlin class Svc { lateinit var repo: Repo fun run() = repo.query() // if repo unset... } Svc().run() // kotlin.UninitializedPropertyAccessException: // lateinit property repo has not been initialized ``` `UninitializedPropertyAccessException` extends `RuntimeException` (unchecked). The generated getter checks the backing field against an internal not-set marker and throws if it hasn't been assigned. The message names the property, which makes diagnosis quick. ## Why fail-fast is the point `lateinit` deliberately turns a missing-initialization mistake into an immediate, loud crash at the access site. Compare with a property defaulted to a dummy value: that would hide the ordering bug and corrupt behavior silently. The exception is a feature — it pins the error to the real cause. ## Contrast with nullable (`Type?`) | Aspect | `lateinit var x: T` | `var x: T? = null` | |--------|---------------------|--------------------| | Type at call sites | non-null `T` | nullable `T?` | | Read before set | throws `UninitializedPropertyAccessException` | returns `null` | | Handling needed | none (assume present) | `?.`, `?:`, `let`, `!!` | | Intent | always present after setup | may be genuinely absent | | Primitives | not allowed | allowed (`Int?`) | ## Choosing between them - Use **`lateinit`** when the value is logically required and will exist before any use, but can't be set in the constructor (DI, lifecycle, tests). You accept a runtime crash if setup ordering is wrong. - Use **nullable** when absence is a valid state you must handle gracefully. ## Defensive checking If you genuinely can't guarantee order, guard with `::x.isInitialized` rather than catching the exception — exceptions for control flow are an anti-pattern.

  • Is `UninitializedPropertyAccessException` checked or unchecked?
    Unchecked — it extends `RuntimeException`, so the compiler doesn't force you to declare or catch it.
  • Should you catch the exception to handle unset state?
    No. Prefer `::prop.isInitialized` to test state; using exceptions for normal control flow is an anti-pattern and obscures real bugs.

saying these in an interview costs you the question

  • Saying early read returns null instead of throwing
  • Claiming the exception is checked
  • Recommending try/catch over isInitialized for normal flow
  • Not knowing the message names the property
  • Conflating lateinit semantics with nullable semantics

context