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

  • lazy is for read-only (val) properties that you want to initialize on first access.
  • lateinit is 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 var and 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 lateinit with primitives: not allowed. Use nullable or default values instead.
  • Overusing lateinit: if you can initialize in the constructor, do it there.
  • Using lazy when a value can change: lazy is for val only.

Best practices

  • Prefer constructor initialization when possible.
  • Use lazy for expensive, read-only values.
  • Use lateinit when framework lifecycle forces delayed assignment.
  • Guard lateinit usage with ::property.isInitialized if 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.