skip to content

You have a `when` expression over a `sealed interface`. Should you add an `else` branch? What are the trade-offs of including vs omitting it?

level: middleimportance: must knowfreq 65%

answer

  1. Omit else on sealed/enum -> compiler flags new cases
  2. else = silent fallthrough for new subtypes
  3. is-branches smart-cast inside the branch
  4. Sealed + exhaustive when = ADT / tagged union
  5. else only for unbounded types or true default

basics

~20 s

Usually omit the else. Then if someone adds a new subtype later, the code stops compiling and points you to every when you must update. Adding an else hides those spots and lets bugs slip through.

solid answer

~50 s

Prefer omitting `else` for `when` expressions over sealed hierarchies (and enums). When you list every subtype, the `when` is exhaustive without `else`, and the compiler verifies coverage. The real payoff comes later: adding a new sealed subtype or enum entry makes every exhaustive `when` that omitted `else` **fail to compile**, forcing you to handle the new case everywhere. Adding `else` defeats this — the new case silently falls into `else`, which is a common source of latent bugs. Use `else` only when there genuinely is a sensible default for unanticipated cases, or when the subject is an open/unbounded type (`Int`, `String`) where exhaustiveness without `else` is impossible. For sealed subjects you typically branch with `is` checks (which also smart-cast), e.g. `is Loading`, `is Success`, `is Error`. This is why sealed classes plus exhaustive `when` are the idiomatic Kotlin replacement for tagged unions / algebraic data types.

code

kotlin · 9 lines
kotlin
sealed interface Shape
data class Circle(val r: Double) : Shape
data class Square(val s: Double) : Shape

fun area(shape: Shape): Double = when (shape) {
    is Circle -> Math.PI * shape.r * shape.r
    is Square -> shape.s * shape.s
    // no else: add a new Shape subtype and this won't compile
}

go deeper

for a junior

Knows sealed when can skip else and that is lets you access subtype members.

for a middle

Explains the omit-else trade-off and the compile-fail-on-new-subtype safety property clearly.

for a senior

Discusses when else is genuinely warranted (unbounded types, cross-module sealed) and ties it to smart-casting.

for a principal

Frames sealed+exhaustive-when as ADT modeling and an architectural choice that makes the compiler enforce total domain handling.

## The recommendation: omit `else` on sealed/enum `when` expressions A **`sealed interface`** (or `sealed class`) has a **closed set of direct subtypes** known to the compiler. Listing each subtype makes a `when` **exhaustive without `else`**. ```kotlin sealed interface UiState data object Loading : UiState data class Success(val data: String) : UiState data class Error(val message: String) : UiState fun render(state: UiState): String = when (state) { Loading -> "..." is Success -> state.data // smart-cast to Success here is Error -> state.message } ``` The `is` checks **smart-cast** `state` to the matching subtype inside each branch, so you can read subtype-specific properties without an explicit cast. ## Trade-off: omit `else` (recommended) - **Pro:** Adding a new subtype (say `data object Empty : UiState`) makes this `when` **fail to compile** with "`when` expression must be exhaustive, add necessary branches". The compiler becomes a checklist of every site to update. - **Pro:** No silent default swallowing unhandled cases. - **Con:** You must update every exhaustive `when` when the hierarchy grows (this is the feature, not a bug). ## Trade-off: add `else` - **Pro:** Tolerates future subtypes without recompilation errors — useful if the default is genuinely correct (e.g. "ignore unknown"). - **Con:** New subtypes silently fall into `else`, hiding cases that may need distinct handling — a frequent latent-bug source. ## When `else` is legitimately right - The subject is **unbounded** (`Int`, `String`, open class) — exhaustiveness without `else` is impossible. - There is a true catch-all default that should apply to *any* not-yet-known case. - A library sealed type marked so consumers cannot exhaustively match across module boundaries; then a defensive `else` is reasonable. ## Bigger picture: ADTs Sealed hierarchy + exhaustive `when` = Kotlin's **algebraic data type / tagged union** pattern. The compiler guarantees **total** handling, which is the main reason to model domain states as sealed types rather than nullable flags or strings.

  • What concrete benefit do you lose by adding `else` to a sealed `when`?
    You lose the compile-time error when a new subtype is introduced; the new case silently routes to `else` instead of being flagged for explicit handling.
  • Do `is` branches in a sealed `when` smart-cast?
    Yes. Inside an `is Success` branch the subject is smart-cast to `Success`, so you can access its properties without an explicit cast (assuming it's a stable, non-mutable reference).

Omitting else is like a smoke alarm that goes off when something new appears; adding else is like taping over the alarm so it stays quiet no matter what.

saying these in an interview costs you the question

  • Always adding `else` 'to be safe' on sealed when expressions
  • Not realizing omitting else gives compile-time coverage checks
  • Thinking `else` and exhaustiveness are interchangeable
  • Unaware that `is` branches smart-cast
  • Calling sealed+when a runtime check rather than compile-time

context