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.
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)
lateinitis 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 errorTo 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 errorDevelopers 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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
AndroidPub
Senior Android Developer & Interviewer, regularly sharing original tech articles, learning resources, and practical interview guides. Welcome to follow and contribute!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
