Home › Mobile › Kotlin lateinit Crash: Property Accessed Before It Exists
Beginner 6 min · September 23, 2026
Kotlin lateinit Property Has Not Been Initialized

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..

N
Naren Founder & Principal Engineer

20+ years shipping production backend systems. Written from production experience, not tutorials.

Follow
✓ Production
production tested
September 27, 2026
last updated
2,085
articles · all by Naren
Before you start⏱ 9 min
  • ✓Basic Kotlin properties, including val vs var and nullability
  • ✓An Android project with activities or fragments
  • ✓Comfort reading logcat crash logs
 ● Production Incident 🔎 Debug Guide
⚡Quick Answer
  • 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
✦ Definition~90s read
What is Kotlin lateinit Property Has Not Been Initialized?

lateinit is a promise you make to the Kotlin compiler: 'This non-null var will be assigned before anyone reads it, so don't force me to initialize it now.' The compiler accepts the deal and skips definite-assignment analysis for that property. At runtime, Kotlin tracks a hidden initialized flag per lateinit property, and any read before assignment throws UninitializedPropertyAccessException — a RuntimeException whose message names the property but not the missing call that should have assigned it.

★
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.

The promise exists because frameworks genuinely assign properties outside your constructor. Android creates activities and fragments itself, then calls your lifecycle methods; dependency injection frameworks populate fields after construction; test harnesses swap collaborators between cases.

Without lateinit, you'd initialize everything to null or dummy values and scatter null checks through code that's logically non-null after setup. lateinit keeps that code clean — at the price of a crash if your timing assumption breaks.

The language constrains the deal: lateinit allows only var (never val), only non-null reference types (never primitives like Int), and only properties with no custom getter/setter. Readiness is queryable through ::prop.isInitialized, which enables graceful handling at integration seams.

The two escape hatches cover the rest: by lazy creates self-initializing thread-safe vals for single-assignment values, and nullable vars (Type? = null) model states where absence is normal. Most lateinit crashes resolve to one of three repairs: prove the ordering, check isInitialized at the seam, or switch to the delegate that can't throw.

Plain-English First

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.

ProfileActivity.ktKOTLIN
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class ProfileActivity : AppCompatActivity() {
    lateinit var repo: UserRepo

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        repo = UserRepo(api) // assignment must run before any read
        loadProfile()
    }

    private fun loadProfile() {
        // SAFE here: repo was assigned two lines ago
        repo.getProfile().observe(this) { show(it) }
    }

    // UNSAFE: a callback registered elsewhere could call this first
    fun onDeepLink() {
        if (::repo.isInitialized) loadProfile()
        else Log.w("Profile", "deep link before setup; ignoring")
    }
}
📊 Production Insight
Nearly every lateinit crash trace is easy to read and hard to fix, because the missing assignment ran late or never — never at the crash line.
🎯 Key Takeaway
lateinit trades a compile-time check for a runtime flag. The crash names the property, but the bug is the ordering that skipped assignment.

::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.

SearchFragment.ktKOTLIN
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class SearchFragment : Fragment() {
    private lateinit var adapter: ResultAdapter

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        adapter = ResultAdapter() // created BEFORE any observer runs
        binding.list.adapter = adapter
        // Observer registered after adapter exists: safe on any device
        viewModel.results.observe(viewLifecycleOwner) {
            if (::adapter.isInitialized) adapter.submit(it)
        }
    }

    fun refreshFromWidget(data: List<Result>) {
        // Seam entry: ordering can't be proven, so check first
        if (::adapter.isInitialized) adapter.submit(data)
    }
}
📊 Production Insight
Registering observers before creating adapters is the number-one fragment ordering bug; creating state first then subscribing fixes it permanently.
🎯 Key Takeaway
Check isInitialized at external seams, fix ordering in your own code, and scope fragment observers to viewLifecycleOwner.

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.

OrderView.ktKOTLIN
1
2
3
4
5
6
7
8
9
10
11
12
13
// BEFORE: crash-prone lateinit for a single-assignment value
// lateinit var dateFormat: SimpleDateFormat

// AFTER: self-initializing, thread-safe, no crash path
private val dateFormat by lazy { SimpleDateFormat("yyyy-MM-dd", Locale.US) }

// Nullable alternative when 'no value yet' is a normal state
private var selectedId: String? = null

fun render(order: Order?) {
    // Null is explicit here: missing selection is handled, not crashed on
    titleView.text = order?.selectedId ?: selectedId ?: "No order selected"
}
📊 Production Insight
Teams that convert single-assignment lateinits to lazy during review watch this crash category shrink to nearly zero within a quarter.
🎯 Key Takeaway
Assigned once means by lazy; legitimately absent means nullable var; only framework-assigned state keeps lateinit.

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.

CartScreen.ktKOTLIN
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ViewModel: no lateinit needed when deps come via constructor
class CartViewModel(private val repo: CartRepo) : ViewModel() {
    val total: LiveData<Int> = repo.total()
}

// Activity: lazy view binding, ready whenever first touched
class CartActivity : AppCompatActivity() {
    private val binding by lazy { CartBinding.inflate(layoutInflater) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)
        // binding is guaranteed ready here — first read inflates it
    }
}
📊 Production Insight
Constructor-injected ViewModels are trivially unit-testable; field-injected lateinit ViewModels need framework harnesses that slow every test down.
🎯 Key Takeaway
Construct ViewModels with real dependencies, init activities top-down in onCreate, and scope fragment bindings to the view lifecycle.

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.

⚠ Delayed Posts Hide the Race
Don't 'fix' ordering crashes by moving reads into postDelayed or arbitrary handlers. You'll convert a loud crash into a silent race that fails on different devices instead. Fix the order or change the delegate.
📊 Production Insight
Process-death lateinit crashes spike after OS updates that kill background apps more aggressively — always retest on new OS betas.
🎯 Key Takeaway
Recreated processes lose static state, every view constructor needs coverage, and test-only swaps shouldn't dictate production design.

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.

📊 Production Insight
The assignment comment rule catches more ordering bugs in review than any amount of post-crash log reading ever will.
🎯 Key Takeaway
Require an assignment comment on every lateinit, test the earliest read path, and audit quarterly to convert stragglers to lazy.
● Production incidentPOST-MORTEMseverity: high

A LiveData Observer Fired Before the Fragment Was Ready

Symptom
Crashlytics showed UninitializedPropertyAccessException on the product screen's adapter, climbing to 2% of sessions within a day of release. Traces varied slightly across OS versions but always named the same property. Support saw one-star reviews saying the product page 'never opens' on older phones.
Assumption
The team blamed the crash on low-end devices and memory pressure because it never reproduced on office phones. QA passed the release since their devices were fast enough that setup always won the race.
Root cause
The fragment registered its LiveData observer in onCreate, but the lateinit adapter it touched was created in onViewCreated. On slow devices the cached LiveData value delivered synchronously during registration — before onViewCreated ran — so the observer read an unassigned lateinit var and threw.
Fix
The fix moved adapter creation into onViewCreated before any observer was registered, and converted the single-assignment formatter to by lazy. A regression test enables 'Don't keep activities,' rotates the device, and asserts no crash. The lateinit survived only for the genuinely framework-assigned binding.
Key lesson
  • 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.
Production debug guideFive checks that pin the missing assignment in minutes.5 entries
Symptom · 01
Crash names a lateinit property but you can see the assignment in code
→
Fix
Grep the codebase for every assignment to that property. If the only assignment lives in onCreate/onViewCreated/setup, check whether the crashing call can run first — callbacks, observers, and deep-link handlers often do. Add a Log.d of ::prop.isInitialized just before the read and reproduce.
Symptom · 02
Crash happens only on some devices or after rotation
→
Fix
Add temporary logs in each lifecycle callback (onCreate, onCreateView, onViewCreated, onStart) plus one at the crash site. Reproduce on a slow emulator with 'Don't keep activities' enabled. The log order shows which callback jumped the queue.
Symptom · 03
You suspect a race between setup and a background callback
→
Fix
Set a breakpoint on the property's assignment and another on the crashing read. If the read breakpoint hits first, ordering is broken. Fix by moving the read after initialization or converting the property to by lazy so first read triggers creation.
Symptom · 04
You inherited a screen with five lateinit vars and repeated crashes
→
Fix
Temporarily replace lateinit var with a nullable var initialized to null. Rebuild and exercise the app — every site that needs touching now shows a compile error or an explicit null. Those sites are your audit list; fix each with ordering, lazy, or a default.
Symptom · 05
You need proof the fix holds before shipping the hotfix
→
Fix
Write a Robolectric or instrumented test that creates the component and immediately triggers the early path (callback before setup). Watch it throw the same exception, apply your fix, and keep the test. It now guards the ordering forever.
lateinit Crash Causes, Checks, and Fixes
Root CauseHow to ConfirmFixPrevention
Property read before assignment ranSearch for the assignment; add ::prop.isInitialized log before the readOrder setup first or guard with isInitializedInit in onCreate/onViewCreated; review ordering
Lifecycle callback fires earlyLog lifecycle events vs first property access orderMove access into/after the initializing callbackNever touch views before onCreateView returns
latelit used for a val-style valueCount assignments: exactly one means it should be lazyConvert to by lazy for valsLint rule: lateinit only for true vars
DI field not injected before useBreakpoint the injection call; verify it ran firstConstructor injection or guarded entry pointPrefer constructor injection over field injection
⚙ Quick Reference
4 commands from this guide
FileCommand / CodePurpose
ProfileActivity.ktclass ProfileActivity : AppCompatActivity() {Why lateinit Exists and Exactly When It Throws
SearchFragment.ktclass SearchFragment : Fragment() {
OrderView.ktprivate val dateFormat by lazy { SimpleDateFormat("yyyy-MM-dd", Locale.US) }The Nullable and by lazy Alternatives That Remove the Crash
CartScreen.ktclass CartViewModel(private val repo: CartRepo) : ViewModel() {Safe Patterns for ViewModels, Activities, and View Binding

Key takeaways

1
lateinit defers a check, not the requirement
something must still assign before first read.
2
Use ::prop.isInitialized at seams where ordering can't be proven, then fix the ordering.
3
Single-assignment values belong in by lazy, not lateinit
lazy can't throw uninitialized.
4
Never touch lateinit views before onCreateView returns; clear fragment bindings in onDestroyView.
5
Prefer constructor injection; field injection plus lateinit is the top DI crash recipe.
6
Audit each lateinit
provable order, lazy candidate, or nullable with a sane default.

Common mistakes to avoid

5 patterns
×

Reading a lateinit var from a callback that fires before setup

Symptom
Intermittent UninitializedPropertyAccessException in production, often on slow devices where lifecycle ordering differs from your test phone.
Fix
Gate every use behind ::repo.isInitialized, or better, restructure so creation happens in onCreate/onViewCreated before any consumer runs. You'll remove the crash and the check in one move.
×

Using lateinit for something reassigned or nulled out later

Symptom
Compile errors when you try to clear it, followed by a hacky empty-object sentinel that confuses every reader.
Fix
Replace with private var repo: Repo? = null and handle null at use, or inject via constructor. If only tests need the swap, use a test-only setter instead of bending production design.
×

Declaring lateinit var for a val-style value computed once

Symptom
A property that's assigned in exactly one place but crashes when a second code path reads it first — a val problem wearing var clothes.
Fix
Switch to private val formatter by lazy { ... } when the value never changes after first use. Lazy is thread-safe by default and can't be read too early.
×

Accessing lateinit view references before onCreateView returns

Symptom
Crashes on configuration change and in navigation-component flows where fragments are created but views aren't ready yet.
Fix
Initialize views with by viewBinding/by lazy or findViewById in onViewCreated, and never touch them before that. For fragments, clear binding in onDestroyView to avoid leaks.
×

Combining lateinit with dependency injection without a readiness check

Symptom
App boots fine in debug but crashes in release when injection ordering shifts, with no hint about which binding was missing.
Fix
Prefer constructor injection or by lazy delegates passed at construction. If the framework forces field injection, add an isInitialized guard with a clear error message at the single entry point.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What does lateinit do and what exception fires when you read it too earl...
Q02SENIOR
A fragment crashes reading a lateinit ViewModel from a callback. How do ...
Q03SENIOR
Compare lateinit var with by lazy. When is each the right choice?
Q04SENIOR
Why can't primitives be lateinit, and what's the alternative?
Q05SENIOR
Your team hits lateinit crashes every release. What systemic fix do you ...
Q01 of 05JUNIOR

What does lateinit do and what exception fires when you read it too early?

ANSWER
lateinit tells the compiler 'I'll assign this non-null var before first use, skip the initializer check.' If you read it early, you get UninitializedPropertyAccessException. It works only on var of non-primitive, non-null type, and you can test readiness with ::prop.isInitialized.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
Is checking ::prop.isInitialized considered good style?
02
Can I use lateinit with Int or other primitive types?
03
Should I replace lateinit with by lazy everywhere?
04
Can I declare a local lateinit variable inside a function?
05
What if my property legitimately has no value at startup?
06
Can a lazy property be reassigned after it's computed?
N
Naren Founder & Principal Engineer

20+ years shipping production backend systems. Written from production experience, not tutorials.

Follow
✓ Verified
production tested
September 27, 2026
last updated
2,085
articles · all by Naren
🔥

That's Kotlin. Mark it forged?

6 min read · try the examples if you haven't

←
Previous
Kotlin NullPointerException on Platform Types from Java
2 / 5 · Kotlin
Next
Kotlin Coroutine JobCancellationException — Scope Leaks
→