What are the evaluation semantics and pitfalls of the Elvis operator's right-hand side — side effects, double evaluation, and use in expressions returning Unit?
answer
- left evaluated once
- right lazy/short-circuit
- good for cache-on-miss
- error()/TODO()/throw return Nothing
- don't bury surprising side effects; prefer if for pure effects
basics
~20 sThe right side of ?: runs only when the left is null, so put side effects there carefully. The left side is evaluated once. Watch out when the right side returns Unit or has effects you didn't intend.
solid answer
~40 sElvis evaluates the **left operand exactly once**; the **right operand is evaluated lazily** only when the left is null. This matters for side-effecting or expensive right sides: `getCached() ?: fetchAndStore()` calls `fetchAndStore()` only on a cache miss. A subtle pitfall is using Elvis where the left is a function call with side effects you expect to repeat — it won't. Another is the right side returning `Unit` (e.g. a logging call) when the surrounding context expects a value, which is a type error unless the whole expression is in a statement position. Because the right side accepts `Nothing`, expressions like `?: error("...")` (which calls `error`, returning `Nothing`) are idiomatic. Avoid hiding heavy work or mutation in the right side where readers won't expect conditional execution.
code
kotlin · 8 linesfun getOrFill(cache: Cache, key: String): Value =
cache.get(key) ?: run {
val v = expensiveCompute(key) // runs only on miss
cache.put(key, v) // mutation only on miss
v
}
val mustExist = configValue ?: error("configValue required") // Nothing branchgo deeper
Understands the right side runs only when the left is null.
Explains single left evaluation and lazy right evaluation, and the cache-on-miss pattern.
Reasons about Nothing-returning helpers, Unit-in-value-position errors, and platform-type coercion.
Guides readability/side-effect conventions and reviews shared code for surprising conditional effects in Elvis right-hands.
## Evaluation order and cardinality For `L ?: R`: 1. **`L` is evaluated exactly once.** Its result is checked for null. 2. If `L` is non-null, that value is the result and **`R` is not evaluated at all**. 3. If `L` is null, **`R` is evaluated** and becomes the result. This lazy, short-circuit behavior is the same idea as `||`/`&&` short-circuiting, applied to nullness. ```kotlin fun load(): Data? { println("load"); return cached } val d = load() ?: compute() // "load" prints once; compute() only on null ``` ## Pitfall 1: expecting the left to run multiple times Some write `list.firstOrNull() ?: list.first()` expecting two passes; that's wasteful and `first()` would throw on an empty list anyway. Compute once and reuse. ## Pitfall 2: heavy or mutating right side Hiding expensive or state-mutating work in `R` surprises readers, since it runs conditionally: ```kotlin val token = cache.get() ?: run { val t = remote.fetch() // network call only on miss cache.put(t) // mutation only on miss t } ``` This is fine and idiomatic *when intentional*, but document it; don't bury non-obvious side effects. ## Pitfall 3: Nothing-returning helpers `error(message)`, `TODO()`, and `throw` all return `Nothing`, so they slot into the right side: ```kotlin val cfg = config ?: error("config missing") // throws IllegalStateException val x = maybe ?: TODO("handle null case") ``` ## Pitfall 4: Unit-returning right side in value position If the right side returns `Unit` (e.g. a bare `log.warn(...)` returning Unit) but the expression is assigned to a non-Unit value, it's a type error. In **statement** position it's allowed but reads oddly: ```kotlin name ?: println("no name") // statement form; result type is Any? / Unit ``` Prefer an explicit `if (name == null) println(...)` for pure side-effect-on-null. ## Pitfall 5: nullable platform types When interoperating with Java, a **platform type** (`String!`) can be null at runtime even though the compiler doesn't force a check. `javaValue ?: default` is a safe way to coerce a platform type into a guaranteed non-null Kotlin value. ## Summary Elvis is precise: left once, right lazily. Use the right side for cheap defaults, intentional cache-fill, or `Nothing`-returning guards; avoid burying surprising side effects, and don't use it purely for a side effect when an `if` is clearer.
- How many times is the left operand of `?:` evaluated?Exactly once; its result is null-checked and reused.
- Is `value ?: error("x")` valid, and what does it do?Yes; `error` returns `Nothing`, so it type-checks, and it throws IllegalStateException when value is null.
- Why use `javaValue ?: default` with Java interop?Java values arrive as platform types that may be null at runtime; Elvis coerces them into a guaranteed non-null Kotlin value.
saying these in an interview costs you the question
- Claiming the left side may be evaluated twice
- Thinking the right side always executes
- Using `?:` purely for a side effect where `if` is clearer
- Burying network calls/mutations in the right side without noting conditional execution
- Not knowing error()/TODO() return Nothing