Kotlin lateinit Crash: Property Accessed Before It Exists
Check ::prop.isInitialized or switch to by lazy: lateinit crashes when a read beats the assignment, usually across lifecycle callbacks..
20+ years shipping production backend systems. Written from production experience, not tutorials.
- ✓Basic Kotlin properties, including val vs var and nullability
- ✓An Android project with activities or fragments
- ✓Comfort reading logcat crash logs
- lateinit var skips the compiler's initializer check, so reading it before assignment throws UninitializedPropertyAccessException
- The crash means ordering broke: a callback, observer, or lifecycle event ran before your setup code
- Check readiness with ::prop.isInitialized at seams where you can't prove the order
- Prefer by lazy for vals assigned once — lazy initializes itself on first read and can't throw this error
- You'll end most lateinit crashes by initializing in onCreate/onViewCreated before any consumer can run
Think of lateinit like reserving a hotel room in someone else's name — you promise the guest will check in before anyone asks for them. If a visitor arrives early and asks for that guest, the front desk has nothing to give and turns them away. That's the crash. The safe alternatives: check whether the guest arrived (::prop.isInitialized), only let visitors in after check-in time (correct lifecycle ordering), or switch to a room that magically prepares itself the moment someone knocks (by lazy).
UninitializedPropertyAccessException is Kotlin telling you it trusted your promise and you broke it. lateinit var is a deal with the compiler: skip the initializer check, I'll assign this before anyone reads it. The compiler shakes hands and looks away. Then a callback fires early, a fragment reads a view before onCreateView returns, or DI runs in a different order on a slow device — and your app crashes on a line that looks completely innocent.
The nasty part is the intermittency. It works on your Pixel, passes CI, then crashes for 2% of users whose lifecycle ordering differs. You'll stare at the declaration, see the assignment three methods down, and conclude it can't be null — because technically it isn't null, it was never assigned at all. The stack trace names the property but not the missing call, so the fix isn't at the crash site.
This article makes lateinit boring again. You'll learn exactly when it throws, how ::prop.isInitialized gives you a safe checkpoint, and when to abandon lateinit for nullable vars or by lazy vals. You'll get lifecycle-safe patterns for activities, fragments, and ViewModels that hold up on every device.
Why lateinit Exists and Exactly When It Throws
lateinit exists for one situation: a non-null var you genuinely can't initialize at construction, usually because a framework assigns it later. Dependency injection, view binding, and test setup are the classic cases. The keyword tells the compiler to skip its definite-assignment analysis — the check that normally forces you to initialize every property in the constructor or at declaration. In exchange, the runtime inserts a flag per property, and any read before assignment throws UninitializedPropertyAccessException with the property's name.
That exception message is both helpful and misleading. It names the property, which feels like a diagnosis, but the actual bug is the missing call — the setup method that didn't run, ran late, or ran on a different instance. You'll find the crash in a callback and the assignment in onCreate, and both look correct in isolation. The defect lives in the ordering between them, which no single file shows you. Slow devices, process recreation, and deep links all reorder execution in ways your fast test phone never demonstrates.
Three constraints shape every decision. lateinit works only on var (never val), only on non-null types, and never on primitives like Int. If your value never changes after assignment, those constraints are a hint you're holding the wrong tool — by lazy gives you a val with the same deferred timing and zero crash path. Reserve lateinit for true framework-assigned vars, keep the assignment as early in the lifecycle as possible, and treat every other use as guilty until proven innocent.
::prop.isInitialized and Lifecycle-Safe Ordering
The isInitialized check is your seatbelt at integration seams — places where you can't prove setup ran first. The syntax is a property reference: ::adapter.isInitialized returns true once assignment happened. Use it where external code can reach in early: widget callbacks, push handlers, deep links, and public methods other components call. When the check fails, do something sane: ignore, queue the work, or log a warning with enough context to find the caller.
Don't confuse the seatbelt with the fix. If your own code needs isInitialized on every read, the ordering is wrong and you're paying a tax forever. The real repair is structural: create state holders before registering observers, initialize in onCreate before callbacks can fire, and pass dependencies through constructors where the framework allows it. After restructuring, most checks evaporate because the order becomes provable by reading the code top to bottom.
Fragments deserve special attention because their lifecycle splits creation across methods. Views exist only between onCreateView and onDestroyView, but observers and callbacks don't respect that window automatically. Register observers with viewLifecycleOwner (not the fragment itself) so they die with the view, create adapters in onViewCreated before subscribing, and clear bindings in onDestroyView. That trio eliminates the two classic fragment crashes — reading views too early and leaking them too late — in one pass.
The Nullable and by lazy Alternatives That Remove the Crash
Most lateinit vars are lazy vals in disguise. Audit each one with a single question: how many times is it assigned? If the answer is once, convert it to by lazy and delete a crash path. Lazy computes the initializer on first read, caches the result, and synchronizes by default — which also fixes the threading races that lateinit handles badly. Date formatters, mappers, regexes, and derived configuration are textbook conversions that take thirty seconds each.
When 'no value yet' is a legitimate state rather than a bug, reach for a nullable var instead. A selected ID, a pending filter, or a draft the user hasn't typed are normal absences, not contract violations. Represent them as String? = null and handle the null at use with ?: or early return. The code gets honest: readers see nullability and handle it, instead of seeing non-null lateinit and assuming safety that doesn't exist.
The remaining lateinit uses — injected fields, view bindings, framework callbacks — earn their place by being genuinely assigned outside your control. Even there, shrink the blast radius: make them private, expose only guarded methods, and document who assigns them and when. A private lateinit with one guarded entry point crashes nowhere; a public lateinit touched from six files crashes everywhere. Visibility is a safety feature, so use it.
Safe Patterns for ViewModels, Activities, and View Binding
ViewModels pair badly with lateinit because they outlive the UI that feeds them. A ViewModel created by a factory should receive dependencies through its constructor — no lateinit, no ordering question, testable with plain unit tests. When the framework forces field injection (some DI setups, some navigation args), isolate the lateinit to one private field and expose only methods that check isInitialized with a message naming the missing binding. One guarded seam beats five hopeful reads.
Activities are simpler: initialize in onCreate before anything else runs, in dependency order. Repositories and adapters first, then observers, then UI wiring that consumes them. Deep-link handlers and intent extras get validated before use, not after a crash tells you they were missing. If an activity can be launched from multiple entry points (launcher, notification, widget), trace each path and confirm every one assigns before reading — the path you forgot is the one your users take.
View binding deserves its settled pattern: inflate in onCreate (activities) or onCreateView (fragments), use freely in between, and — for fragments — null the reference in onDestroyView. Lazy inflation in activities removes ordering questions entirely since first read creates the binding. In fragments, prefer a nullable backing property with a non-null getter that fails only if misused after view destruction, which is a programming error worth surfacing loudly rather than a user-state crash.
Hard Cases: Process Death, Custom Views, and Test Hooks
When the crash resists simple ordering fixes, suspect the process lifecycle. Android kills background processes freely; when the user returns, the activity is recreated but your static caches and companion-object state are gone. A lateinit in a companion or a singleton that was assigned in the dead process now throws on first read in the new one. The fix is persistence-aware init: check isInitialized in singletons too, and rebuild from Intent extras or saved state rather than assuming warm-process state.
Custom views add their own wrinkle with multiple constructors. A lateinit assigned in one init path but read from another crashes only for the constructor you didn't test — usually the XML-inflation path versus the programmatic one. Assign in a shared init block or convert to lazy so every construction path is covered. Preview rendering in Android Studio exercises yet another path, so a crash that appears only in the layout preview is still worth fixing.
Finally, watch for test-only reassignment driving production design. If a property is lateinit solely so tests can swap implementations, you've traded production safety for test convenience. Inject the dependency through the constructor with a default, or expose a test-visible setter on a private field. Tests stay flexible, production keeps its guarantees, and nobody's release depends on a test hook behaving.
Keeping lateinit Crashes Out for Good
Prevention beats debugging, and this crash is unusually preventable. Add a lint rule or code-review checklist item: every new lateinit needs a comment naming who assigns it and when. That single sentence forces the author to prove the ordering while writing the code, when it's cheapest. Reviewers should challenge any lateinit with more than one reader or any reader in a callback — those are lazy or nullable candidates wearing a disguise.
Back it with the cheapest high-value test: the early-path test. For each screen with lateinit state, write one test that triggers the earliest possible read — observer fires before setup, deep link arrives before onCreate finishes, rotation recreates mid-load. Robolectric covers most of these without an emulator. When the test passes with your ordering fix or lazy conversion, it guards that ordering against every future refactor that might shuffle it.
Track the metric that matters: UninitializedPropertyAccessException-free sessions per release. It should sit at zero and stay there; any appearance means a new lateinit slipped past review. Pair it with a quarterly audit — grep lateinit, count assignments per property, convert the single-assignment ones to lazy. Fifteen minutes of audit per quarter buys you permanent freedom from a crash that otherwise returns with every new screen.
A LiveData Observer Fired Before the Fragment Was Ready
- Lifecycle races hide on fast devices — test with 'Don't keep activities' and rotation to force the ordering your users hit.
- Observers registered before setup will fire before setup; create adapters and state holders first, then subscribe.
- Single-assignment properties should be lazy: if it can't change, don't give it a crash path.
| File | Command / Code | Purpose |
|---|---|---|
| ProfileActivity.kt | class ProfileActivity : AppCompatActivity() { | Why lateinit Exists and Exactly When It Throws |
| SearchFragment.kt | class SearchFragment : Fragment() { | |
| OrderView.kt | private val dateFormat by lazy { SimpleDateFormat("yyyy-MM-dd", Locale.US) } | The Nullable and by lazy Alternatives That Remove the Crash |
| CartScreen.kt | class CartViewModel(private val repo: CartRepo) : ViewModel() { | Safe Patterns for ViewModels, Activities, and View Binding |
Key takeaways
Common mistakes to avoid
5 patternsReading a lateinit var from a callback that fires before setup
Using lateinit for something reassigned or nulled out later
Declaring lateinit var for a val-style value computed once
Accessing lateinit view references before onCreateView returns
Combining lateinit with dependency injection without a readiness check
Interview Questions on This Topic
What does lateinit do and what exception fires when you read it too early?
Frequently Asked Questions
20+ years shipping production backend systems. Written from production experience, not tutorials.
That's Kotlin. Mark it forged?
6 min read · try the examples if you haven't