Kotlin `lazy` vs `lateinit`: When to Use Each
Kotlin lazy vs lateinit: When to Use Each
Kotlin gives you two common ways to defer initialization of a property: lazy and lateinit. They look similar on the surface, but they solve different problems. This guide explains how they work, when to use each, and the pitfalls to avoid.
The core difference
lazyis for read-only (val) properties that you want to initialize on first access.lateinitis for mutable (var) properties that will be initialized later, but before any access.
lazy: on-demand, read-only
lazy wraps a value in a delegate. The first time you access it, the initializer runs and the value is cached.
val heavyConfig: Config by lazy {
// Runs once, on first access
loadConfigFromDisk()
}
Good for:
- Expensive initialization you want to postpone
- Values that should never change after creation
Notes:
- Default mode is thread-safe (
LazyThreadSafetyMode.SYNCHRONIZED). - You can opt out of synchronization for performance.
val cache by lazy(LazyThreadSafetyMode.NONE) {
LruCache(100)
}
lateinit: promise to initialize later
lateinit lets you declare a non-null var without providing a value immediately.
lateinit var presenter: Presenter
fun setup() {
presenter = Presenter()
}
Good for:
- Dependency injection (e.g., Android Activity/Fragment)
- Values assigned after construction
Notes:
- Only works with
varand non-primitive types. - Accessing before initialization throws
UninitializedPropertyAccessException.
if (::presenter.isInitialized) {
presenter.start()
}
Side-by-side comparison
| Feature | lazy | lateinit | |---|---|---| | Property type | val | var | | Initialization | On first access | Manually, later | | Nullability | Non-null | Non-null | | Thread safety | Configurable | N/A | | Failure mode | None (initializer runs) | Exception if not initialized |
Practical examples
lazy for deferred setup
class UserRepo {
val db by lazy {
Database.connect("users.db")
}
}
lateinit for lifecycle wiring (Android)
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = MainViewModel()
}
}
Common pitfalls
- Using
lateinitwith primitives: not allowed. Use nullable or default values instead. - Overusing
lateinit: if you can initialize in the constructor, do it there. - Using
lazywhen a value can change:lazyis forvalonly.
Best practices
- Prefer constructor initialization when possible.
- Use
lazyfor expensive, read-only values. - Use
lateinitwhen framework lifecycle forces delayed assignment. - Guard
lateinitusage with::property.isInitializedif access timing is uncertain.
Wrap-up
lazy and lateinit are both about delayed initialization, but they serve different needs. Use lazy for on-demand val properties and lateinit for var properties that must be wired after construction. The clearer your intent, the easier your code is to maintain.