Mobile Development 11 min read

Why You Should Stop Misusing Kotlin’s lateinit and Let the Compiler Catch Bugs

The article explains how Kotlin's lateinit can silently bypass compile‑time null safety, leading to UninitializedPropertyAccessException crashes, and argues that using nullable types with safe‑call operators provides compile‑time guarantees, reduces runtime bugs, and improves code safety.

AndroidPub
AndroidPub
AndroidPub
Why You Should Stop Misusing Kotlin’s lateinit and Let the Compiler Catch Bugs

Why you should stop using lateinit and switch to nullable types

Many Android developers have experienced a late‑night crash caused by

kotlin.UninitializedPropertyAccessException: lateinit property variable has not been initialized

. The author shows that lateinit acts like a wolf in sheep’s clothing, letting a bug slip past Kotlin’s null‑safety checks and appear only at runtime.

1. The two “enemies”: NPE vs. UPAE

NullPointerException (NPE)

In both Java and Kotlin, accessing a null reference causes an immediate crash. Example in Java:

String name = null;
if (name.isEmpty()) { // compile‑time passes, runtime crashes
    System.out.println("Hello");
}

UninitializedPropertyAccessException (UPAE)

lateinit

is a Kotlin‑only feature. Declaring a variable with lateinit and accessing it before it is assigned throws UPAE, moving a potential bug from compile time to runtime.

2. Kotlin’s null‑safety and the “escape” of lateinit

Kotlin prevents assigning null to a non‑nullable type:

var name: String = null // ❌ compile‑time error

To allow null, you must declare a nullable type: var name: String? = null // ✅ compiles When a variable is nullable, the compiler forces you to use safe‑call ?. or non‑null assertion !!:

name?.length // ✅ safe, returns null if name is null
name.length   // ❌ compile‑time error

Developers often choose lateinit to avoid the extra ?. boilerplate, but this creates a “gentleman’s agreement” with the compiler that the variable will be initialized before first use. If the agreement is broken, the app crashes at runtime.

3. Traditional patch: manual isInitialized checks

After a crash, developers may add a runtime check:

override fun onResume() {
    super.onResume()
    // Check if already initialized
    if (::name.isInitialized && name.length == 3) {
        println("Hi")
    }
}

This approach is cumbersome, easy to forget, and defeats Kotlin’s design goal of compile‑time safety.

4. Code review insight: let the compiler be the gatekeeper

Expert: “Why not declare the property as nullable instead of using lateinit ?” Author: “Changing to String? would require many ?. or !! edits, which feels tedious.” Expert: “If a new teammate forgets to add the isInitialized check, the bug will slip to production. With a nullable type, the compiler forces handling at compile time.”

This conversation highlights that compile‑time enforcement is safer than ad‑hoc runtime checks.

5. Direct comparison

Error timing : lateinit → runtime crash; nullable → compile‑time error.

Safety guarantee : lateinit relies on developer discipline; nullable relies on compiler, 100% coverage.

Code appearance : lateinit needs verbose ::var.isInitialized; nullable uses concise ?. or ?: operators.

Development burden : lateinit is simple to declare but risky to maintain; nullable adds a small declaration cost but gives compiler‑backed safety.

6. Correct handling patterns

When using nullable types, avoid the non‑null assertion !!. Prefer safe calls and Elvis operator:

// ❌ Bad: same risk as lateinit
name!!.length

// ✅ Good: safe call with default
val len = name?.length ?: 0

// ✅ Good: use let for scoped handling
name?.let { safeName ->
    if (safeName.length == 3) {
        println("Hi")
    }
}

This extra code guarantees that even if name is null in edge cases, the app will not crash.

7. When lateinit is acceptable

Dependency Injection (e.g., Dagger/Hilt, Koin): @Inject lateinit var viewModel: MyViewModel The framework injects the instance before any business method runs.

View binding tied to the Activity lifecycle:

private lateinit var binding: ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)
}

Since onCreate is guaranteed to run before any use, the property is safely initialized.

8. Core golden rules (Key Takeaways)

Rule 1: If a property may be null at any point, declare it as nullable ( ?).

Rule 2: Use lateinit only when you can guarantee 100 % that the property is initialized before first access (e.g., DI or lifecycle‑bound binding).

Rule 3: In everyday code, avoid the non‑null assertion !!; prefer ?.let or ?: for safer handling.

Conclusion

The safest approach is to let the compiler enforce null‑safety by using nullable types and Kotlin’s safe‑call mechanisms, reserving lateinit for scenarios where initialization timing is guaranteed by the framework.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

AndroidKotlinbest practicesNullPointerExceptionnullablelateinitUninitializedPropertyAccessException
AndroidPub
Written by

AndroidPub

Senior Android Developer & Interviewer, regularly sharing original tech articles, learning resources, and practical interview guides. Welcome to follow and contribute!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.