What are the common correctness pitfalls when refactoring a long `if`/`else if` chain into `when`, particularly around branch order, equality, and side effects?
answer
- First match wins — order = semantics
- Subject evaluated once
- `==` structural equality, not `===`
- Match short-circuits later conditions
- Blanket `else` can hide missing cases
basics
~20 sBranch order matters because the first match wins, so overlapping conditions can change behavior if reordered. Also remember when uses structural equality (==), evaluates the subject once, and stops at the first match — so don't rely on later branches' side effects.
solid answer
~50 sRefactoring an `if`/`else if` ladder into `when` must preserve **branch order**: `when` runs the first matching branch top-to-bottom, so overlapping ranges or conditions can silently change results if reordered (e.g. `in 0..100` before `in 50..100`). The subject is **evaluated once**, which differs from repeating a function call in each `if` — a behavior change if that call had side effects or returned different values. Subject branches use **structural equality `==`** (which calls `equals`), not reference equality, matching `if (x == ...)`. Because evaluation **short-circuits** at the first match, side effects in later conditions won't run — fine if conditions are pure, a bug if they were doing work. Watch nullability: `null` is a valid branch value and the subject form null-checks via `==`. For the expression form, ensure exhaustiveness so you don't accidentally need an `else` that masks a missing case.
code
kotlin · 6 lines// Capture side-effecting subject once; keep narrow-before-broad order
fun bucket(iter: Iterator<Int>): String = when (val n = iter.next()) {
in 0..9 -> "low"
in 0..99 -> "mid" // broader range AFTER the narrow one
else -> "high/neg"
}go deeper
Understands first-match-wins and that order matters for overlapping conditions.
Knows the subject is evaluated once and that branches use ==; handles null correctly.
Anticipates side-effect and ordering bugs during refactors and reasons about exhaustiveness trade-offs.
Sets review standards: capture side-effecting subjects, enumerate sealed cases over else, and treat branch order as part of the contract.
## 1. Branch order is semantics, not style `when` evaluates branches **top to bottom** and runs the **first** match. If branches overlap, order determines the result: ```kotlin // BUG if reordered: when (score) { in 90..100 -> "A" in 0..100 -> "pass" // would swallow 90..100 if placed first else -> "invalid" } ``` When translating an `if`/`else if` chain, keep the original order. The most specific/narrow conditions must come **before** broader ones. ## 2. The subject is evaluated exactly once ```kotlin when (next()) { // next() called ONCE 1 -> ... 2 -> ... } ``` An `if (next() == 1) ... else if (next() == 2) ...` ladder calls `next()` **multiple times**. If the call has side effects or is non-deterministic, switching to a subject `when` changes behavior. Capture the value with `when (val v = next())` to be explicit. (Conversely, going from a subject `when` to an `if` ladder that re-invokes the expression introduces extra calls.) ## 3. Equality is structural (`==`) Subject branches compare with `==`, i.e. `equals` (structural), not `===` (reference). This matches typical `if (x == y)` semantics but differs from a hypothetical reference check. For data classes and strings this is usually what you want. ## 4. Short-circuit and side effects Once a branch matches, **no later branch condition is evaluated**. If your original chain relied on side effects inside later `else if` conditions, those won't run after an earlier match (same as `else if`, but easy to overlook when conditions are function calls). ## 5. Nullability In a subject `when`, `null` is a legal branch label and the subject is compared with `==`, which is null-safe. Handle `null` explicitly or via `else`: ```kotlin when (name) { null -> "anonymous" "" -> "empty" else -> name } ``` ## 6. Expression form and exhaustiveness When you turn the chain into a `when` **expression** (assigning its value), Kotlin requires it to be exhaustive. Reaching for a catch-all `else` can hide a genuinely missing case (especially with sealed types where omitting `else` would have produced a compile-time error pointing at the gap). Prefer enumerating subtypes over `else` for sealed/enum subjects so new cases break the build. ## 7. Performance note (minor) Dense integer/enum subject `when`s may compile to a `tableswitch`/`lookupswitch`; subjectless or type-based ones become conditional chains. Rarely a correctness concern, but worth knowing for hot paths. ## Summary checklist - Preserve branch order; narrow before broad. - Subject is evaluated once — beware side-effecting calls. - `==` structural equality. - Later conditions don't run after a match. - Handle `null` explicitly. - Prefer exhaustive enumeration over a blanket `else` for sealed/enum.
- Why can reordering branches change behavior?Because `when` runs the first matching branch; overlapping conditions resolve differently depending on which appears first.
- How many times is the subject expression evaluated?Exactly once, unlike re-invoking the same call in each arm of an `if`/`else if` ladder.
- Why might omitting `else` be safer for a sealed subject?Without `else`, adding a new subtype makes the `when` non-exhaustive and fails compilation, forcing you to handle it.
saying these in an interview costs you the question
- Reordering overlapping branches assuming behavior is unchanged
- Assuming the subject is re-evaluated per branch
- Confusing `==` and `===` semantics in branches
- Adding a blanket `else` to a sealed `when` and losing compile-time exhaustiveness
- Forgetting to handle `null` explicitly