skip to content

What does `val x by lazy { ... }` do in Kotlin, and when does the lambda run?

level: juniorimportance: must knowfreq 78%

answer

  1. Computed once on first read, then cached
  2. Backed by Lazy<T> delegate via `by`
  3. val only (getter, no setter)
  4. Default mode is SYNCHRONIZED (thread-safe)
  5. Great for expensive / order-sensitive init

basics

~10 s

It delays creating a value until the first time you read the property. The block runs once, the result is remembered, and every later read returns that same stored value.

solid answer

~30 s

`by lazy { ... }` uses the standard-library `lazy()` function to create a `Lazy<T>` delegate. The property's getter is backed by that delegate: the first time you read the property, the lambda is executed and the result is cached in the delegate's `value`. All subsequent reads return the cached value without re-running the lambda. It only works on a `val` (read-only), since the value is computed once and never reassigned. Common uses: expensive objects you may not always need, breaking initialization order/cycles, or values that depend on something not yet available at construction time. By default it is thread-safe (`LazyThreadSafetyMode.SYNCHRONIZED`).

code

kotlin · 10 lines
kotlin
class Service {
    val client by lazy {
        println("building client")
        HttpClient()           // runs once, on first use
    }
}

val s = Service()              // nothing printed yet
s.client                       // prints "building client"
s.client                       // prints nothing; cached

go deeper

for a junior

Knows it computes once on first read and caches the result; can write a basic val x by lazy { ... }.

for a middle

Explains the Lazy<T> delegate, why it's val-only, and contrasts it with lateinit and eager init.

for a senior

Adds default SYNCHRONIZED thread-safety, exception-retry semantics, and when laziness helps with init order.

for a principal

Frames trade-offs: per-access null-check/sync cost vs. eager init, GC of the captured lambda, and API/library-design implications of exposing lazy values.

## What `by lazy` is `by` is Kotlin's **delegated property** keyword: instead of the property storing a value directly, reads/writes are forwarded to a *delegate* object. `lazy { ... }` is a standard-library factory function that returns a `Lazy<T>` instance, and `Lazy<T>` provides the `getValue` operator that the `by` mechanism calls. ```kotlin val config: Config by lazy { println("computing") loadConfig() // runs ONCE, on first access } ``` ## Lifecycle - **Before first read:** the lambda has not run; no value exists. - **First read:** the lambda executes, its result is stored in the delegate. - **Every later read:** the stored value is returned; the lambda is **never** re-run. You can check whether it has been computed via `isInitialized()` on the `Lazy` instance (you need a reference to the delegate, obtainable with the `::config.getDelegate()`-style reflection, or by holding the `Lazy` yourself). ## Why only `val`? `by lazy` requires a `val`. A lazy value is computed once and cached; reassigning it makes no sense, so the `Lazy<T>` delegate only exposes a getter (`getValue`), not a setter. ## Common reasons to use it - **Deferring expensive work** (DB connection, parsing) until actually needed. - **Avoiding null / `lateinit`** for values that need other state to exist first. - **Breaking initialization-order problems** inside a class, since the block runs lazily rather than at construction time. ## Contrast with alternatives - `lateinit var` — mutable, no caching semantics, you must assign it manually; throws if read before assignment. - Eager `val x = expensive()` — computed at construction even if never used. Note: the lambda is captured as part of the delegate until first access, then released so it can be garbage-collected.

  • Can you use `by lazy` on a `var`?
    No. `lazy()` returns a `Lazy<T>` that only provides `getValue`, so the compiler rejects it on a `var`. Lazy values are write-once.
  • Does the lambda run again if it throws on first access?
    With the default SYNCHRONIZED mode, an exception is not cached: the next access retries the lambda. The value is only cached on successful completion.

Like a vending slot that's empty until someone first presses the button; it makes the snack once and then hands out that same snack on every later press.

saying these in an interview costs you the question

  • Says the lambda runs at object construction time
  • Thinks the block re-runs on every property access
  • Claims `by lazy` works on `var`
  • Confuses it with `lateinit` (which has no caching and is mutable)

context