If a `by lazy` initializer throws an exception on first access, what happens on the next access? Does the answer depend on the thread-safety mode?
answer
- Value cached only on successful return
- Exception -> not cached -> next read retries
- Same for SYNCHRONIZED / PUBLICATION / NONE
- Make the block idempotent / retry-safe
- Cache failures by returning Result inside the block
basics
~10 sIf the first computation fails with an exception, the value is not stored. The next time you read the property, Kotlin runs the block again and tries to produce the value once more.
solid answer
~40 sFor all standard `lazy` implementations, the value is only cached on **successful** completion of the initializer. If the lambda throws, no value is set, so the next access **re-runs** the initializer (and may throw again or succeed). This is true for SYNCHRONIZED, PUBLICATION, and NONE — none of them caches the thrown exception. Practical consequences: a `lazy` block must be safe to retry; side effects inside it (e.g. partially mutating shared state) can run multiple times if it keeps failing. With SYNCHRONIZED, retries are serialized under the lock; with PUBLICATION, concurrent threads can each retry; with NONE there's no coordination at all. If you need 'compute once, remember the failure', you must capture the result/exception yourself (e.g. wrap in a `Result` and cache that).
code
kotlin · 8 linesprivate var calls = 0
val cfg: Config by lazy {
calls++
require(envReady()) { "env not ready" } // may throw
loadConfig()
}
// Each failed read increments `calls` and re-runs the block;
// only a successful run caches the Config.go deeper
Can say the value isn't stored if the block fails and gets recomputed next time.
Explains success-only caching and that retries happen, with a basic retry-safety caveat.
Distinguishes caching (mode-independent) from retry coordination (mode-dependent) and recommends idempotency or Result-caching.
Reasons about failure-domain design: negative caching, backoff, side-effect safety, and exposing outcomes as data for resilient initialization.
## The caching rule: success-only Kotlin's `Lazy` implementations store the computed value **only when the initializer returns normally**. An exception propagates to the caller and leaves the delegate in its **uninitialized** state. Therefore: ```kotlin var attempts = 0 val value: Int by lazy { attempts++ if (attempts < 3) error("not yet") // throws on attempts 1 and 2 42 } runtimeCatching { value } // attempt 1 -> throws runtimeCatching { value } // attempt 2 -> throws value // attempt 3 -> 42, now cached ``` Each failing read re-invokes the block; the third succeeds and caches `42`. ## Does the mode change this? The **success-only caching** is the same for `SYNCHRONIZED`, `PUBLICATION`, and `NONE`. What differs is **concurrency around the retry**: - **SYNCHRONIZED**: the lock serializes attempts; one thread retries at a time, others block, then see either the cached value or another failure. - **PUBLICATION**: multiple threads may attempt concurrently; a successful result is published via CAS; failures just propagate and leave it uninitialized for future tries. - **NONE**: no coordination; retries are plain re-executions on whatever thread reads. ## Why this matters (design implications) - **Idempotency**: a `lazy` block can run more than once across failed attempts, so avoid non-idempotent side effects (writing files, incrementing counters) inside it. - **Repeated expensive failures**: if computation is costly and keeps failing, every access pays the full cost again — there's no built-in backoff or negative caching. - **Caching the failure deliberately**: wrap the computation so the *outcome* is what's cached: ```kotlin val result: Result<Config> by lazy { runCatching { loadConfig() } } // Now both success and failure are remembered; .getOrThrow() to surface it. ``` Here the `lazy` block itself never throws (it returns a `Result`), so it completes successfully and the failure is cached as data. ## Summary A thrown initializer does **not** poison the delegate; the next read retries. The thread-safety mode affects only how concurrent retries are coordinated, not the success-only caching contract.
- How can you make a lazy property remember a failure instead of retrying?Have the block return a captured outcome, e.g. `by lazy { runCatching { compute() } }`. The block then always returns normally, so the Result (success or failure) is cached; call `.getOrThrow()` at the use site.
- Does PUBLICATION cache the exception of the first thread that fails?No. PUBLICATION only publishes a successful value via CAS; a failing initializer leaves the delegate uninitialized, so subsequent accesses retry.
saying these in an interview costs you the question
- Says the exception is cached and re-thrown on every later access
- Claims the property becomes permanently broken after a throw
- Thinks behavior on exception differs fundamentally by mode (it doesn't for caching)
- Puts non-idempotent side effects in the block assuming single execution
- Believes a thrown initializer stores a null/default value