skip to content

Why does Kotlin make if/when/try expressions instead of statements, and what design trade-offs come with leaning on this?

level: seniorimportance: should knowfreq 40%

answer

  1. Expressions replace var+reassign with a single val
  2. Pairs with val-first and single-expression functions
  3. Expression form forces exhaustiveness (else / sealed)
  4. Risk: dense nested value-returning branches
  5. Use statement form for side-effect-heavy flow

basics

~10 s

Making control structures return values lets you assign results directly and skip temporary mutable variables, leading to shorter, safer code. Overusing it, though, can make logic hard to read.

solid answer

~40 s

Treating `if`/`when`/`try` as expressions lets each control structure yield a value, so you initialize a `val` in one shot instead of declaring a `var` and reassigning it across branches. This dovetails with val-first immutability and single-expression functions, shrinking mutable state and the surface for bugs (uninitialized vars, forgotten branches). The compiler can enforce **exhaustiveness** (every branch must yield a value, `else` required, sealed/enum coverage checked), turning gaps into compile errors. The trade-off: deeply nested expression-`when`/`if` returning values can hurt readability, and pushing side effects into expression branches mixes concerns. Senior judgment is choosing expression form for value selection and statement form for side-effecting control flow, and extracting complex branching into well-named functions rather than one giant expression.

code

kotlin · 7 lines
kotlin
// expression form: one val, exhaustiveness enforced for sealed Tier
val discount: Double = when (tier) {
    Tier.GOLD -> 0.2
    Tier.SILVER -> 0.1
    Tier.BRONZE -> 0.0
}
// adding Tier.PLATINUM later -> compile error here until handled

go deeper

for a junior

Knows if/when/try can return values and that this avoids some temporaries.

for a middle

Explains the var-to-val improvement and that expression form enforces exhaustiveness.

for a senior

Weighs readability and side-effect trade-offs and chooses expression vs statement form deliberately, extracting complex branches.

for a principal

Articulates the consistency/constrain-mutable-state rationale and sets team guidance on when expression-orientation aids or harms maintainability.

## What "expression-oriented" buys you When `if`, `when`, and `try` are **expressions** (yield values), you can assign the result of a branch directly: ```kotlin // expression-oriented: one immutable binding val rate = when (tier) { Tier.GOLD -> 0.2 Tier.SILVER -> 0.1 Tier.BRONZE -> 0.0 } ``` versus the statement-oriented alternative that needs a mutable temporary: ```kotlin var rate = 0.0 // mutable, can be left wrong when (tier) { Tier.GOLD -> rate = 0.2 Tier.SILVER -> rate = 0.1 Tier.BRONZE -> rate = 0.0 } ``` Benefits: - **Fewer `var`s** — pairs with **val-first** immutability; the binding is assigned exactly once. - **Compiler-checked completeness** — as an expression, every path must yield a value, so `else` is required, and over enums/sealed types missing cases are compile errors. - **No "uninitialized/partially set" states** — the value can't be observed half-built. - **Composability** — the result flows into `val`s, returns, and single-expression functions (`fun f() = when (...) { ... }`). ## Trade-offs and judgment - **Readability ceiling**: a large `when`/`if` that *returns* a value and also nests other expressions can become dense. Extract branches into named functions. - **Side effects in branches**: expressions are best for *choosing a value*. When branches mostly *do* things (I/O, logging), statement form reads more honestly; mixing both blurs intent. - **Type unification surprises**: the result type is the least common supertype of branches; heterogeneous branches can widen to `Any?` unexpectedly. - **`try` as expression**: convenient for parse-or-default, but heavy error handling embedded in an assignment can hide control flow. ## Design framing Kotlin chose expression-orientation for **consistency** (one model across `if`/`when`/`try`) and to **constrain mutable state**, which improves local reasoning, reviewability, and refactoring safety. The senior skill is applying it where it clarifies (value selection, exhaustiveness over domain models) and avoiding it where it obscures (side-effect-heavy flow, deep nesting).

  • When would you deliberately use statement form instead of expression form?
    When branches are primarily side effects (logging, I/O, mutation) and there's no single value to return. Statement form reads more honestly and avoids forcing an artificial result.
  • How does expression-orientation reduce a class of bugs?
    By eliminating mutable temporaries and requiring every branch to yield a value, it removes uninitialized/partially-set variables and, over sealed/enums, turns missing cases into compile errors.

It's the difference between filling in a form field once with the final answer (expression) versus scribbling, erasing, and rewriting the same field (var reassignment).

saying these in an interview costs you the question

  • Claiming expression-orientation is purely cosmetic
  • Cramming heavy side effects into value-returning branches
  • Ignoring the readability cost of deep nested expressions
  • Not connecting it to val-first immutability and exhaustiveness
  • Assuming branch type unification never surprises you

context