skip to content

What are the timing and correctness pitfalls of provideDelegate — e.g., touching thisRef state inside it, ordering with other initializers, and the cost of throwing there?

level: seniorimportance: nice to knowfreq 10%

answer

  1. runs during construction -> thisRef half-built
  2. later fields/init blocks still at defaults
  3. throwing aborts construction (and may leak side effects)
  4. eager: paid even if never read
  5. keep it pure/cheap; use lazy for I/O

basics

~20 s

It runs while the owning object is still being built, so the object may be only partly initialized. Reading other fields from thisRef can give defaults or nulls. Throwing inside it aborts construction. Keep it side-effect-light and don't rely on later fields.

solid answer

~40 s

provideDelegate runs at delegate-creation time, which for a member property is during the constructor/initializer flow in declaration order. So thisRef may be partially constructed: properties declared after this delegated property (or initialized later in init blocks) are not yet set, and reading them can yield default/null values. Throwing in provideDelegate propagates out of the constructor — useful for fail-fast invariants but it means a bad declaration prevents object creation entirely (and may leak partially built state if you have side effects). Avoid heavy work or external I/O here; it runs eagerly even if the property is never read. Order matters: if delegate A's provideDelegate needs a field set by an init block placed after it, A sees the unset value. Treat thisRef as not-fully-formed.

go deeper

for a junior

Knows it runs early during object setup and shouldn't do heavy work.

for a middle

Explains partial initialization of thisRef and the eager cost.

for a senior

Reasons about init-block ordering, throw-aborts-construction semantics, and resource-leak risk from side effects.

for a principal

Treats it as a construction-time invariant gate, weighing fail-fast benefits against leak/cleanup and eager-cost trade-offs across many instances.

## Why timing is delicate For a **member** property, the hidden `x$delegate` field is initialized **in declaration order during construction**. `provideDelegate` therefore executes while the enclosing object is **still being built**. This creates several traps. ### 1. thisRef is partially initialized Any property declared *after* the delegated one, or set inside an `init {}` block that runs *later*, has **not been assigned yet**. Reading it from inside `provideDelegate` yields the default value (`0`, `false`, `null` for references). This is the same hazard as leaking `this` from a constructor. ```kotlin class Bad { val base = 10 val derived: Int by Factory() // provideDelegate may read 'late' here... val late = 99 // ...but 'late' is still 0 at that point } ``` ### 2. Declaration ordering with init blocks Initializers (property assignments and `init {}` blocks) run **top to bottom**. If `provideDelegate` depends on state established by a *later* `init {}`, it will see the pre-init value. Reorder so dependencies are initialized first, or avoid the dependency. ### 3. Throwing aborts construction Throwing from `provideDelegate` propagates out of the constructor, so **the object is never successfully created**. That is exactly what you want for fail-fast invariants — but: - Any **side effects** performed earlier in construction (registered listeners, opened resources, started threads) may **leak** because there is no instance to clean them up. Keep provideDelegate side-effect-free, or do cleanup-aware construction. - It converts a lazy/optional failure into a hard construction failure; make sure that is intended. ### 4. Eager cost It runs **whether or not the property is ever read**. Heavy work (I/O, network, large allocations) is paid up front for every instance. If the value might never be used, prefer `by lazy` or logic in `getValue`. ### 5. Local and top-level cases For **local** delegated properties, `provideDelegate` runs when the declaration line executes — surrounding locals declared earlier are available; later ones are not (normal scoping). For **top-level** properties, it runs during file/class initialization; `thisRef` is null so there is no instance state to mis-read. ## Practical guidance - Treat `thisRef` as **possibly half-built**; don't read fields you don't control the ordering of. - Keep it **pure and cheap**: validate names, derive keys, build the delegate — no external I/O or registration side effects. - Use throwing **deliberately** for invariants, knowing it stops construction. - If you need laziness or guaranteed full initialization, move logic to `getValue` or `by lazy`.

  • Why might reading another property of thisRef inside provideDelegate be unsafe?
    Members are initialized in declaration order during construction; a property declared later is still at its default/null when provideDelegate runs.
  • What happens to the object if provideDelegate throws?
    The exception propagates out of the constructor and no instance is produced; any earlier side effects in construction may leak.

saying these in an interview costs you the question

  • Assuming thisRef is fully initialized inside provideDelegate
  • Doing network/file I/O eagerly there for values that may never be read
  • Registering listeners/resources without considering a throw leaking them
  • Ignoring declaration-order dependencies with init blocks

context