skip to content

Exhaustive when over Sealed

A when expression over a sealed type needs no else, because the compiler can verify every subtype is handled — and will fail the build when someone adds one. Each arm also smart-casts, so you get the subtype's own properties for free.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

questions

5

When you use `when` as an expression over a sealed type and cover every subtype, why don't you need an `else` branch?

level: juniorimportance: must knowfreq 70%

answer

  1. Closed subtype set = compiler can enumerate cases
  2. when as expression must be exhaustive
  3. All branches covered → no else
  4. Adding a subtype breaks compile (good)
  5. Each is-arm smart-casts the subject

basics

~10 s

Because the sealed type lists all its possible subtypes in advance. If you handle each one, the compiler knows nothing else is possible, so an else branch would be pointless.

solid answer

~40 s

A `sealed class` or `sealed interface` has a fixed, closed set of direct subtypes known at compile time (all in the same module/package). When you use `when` as an *expression* (its result is used or returned) over such a type and provide a branch for every subtype, the compiler proves the `when` is exhaustive and accepts it without an `else`. This is better than `else` because if you later add a new subtype, the `when` stops compiling, forcing you to handle the new case. Note: `when` used as a *statement* (result ignored) over a sealed type is also checked for exhaustiveness since Kotlin 1.7. Each branch also smart-casts the subject to the matched subtype.

code

kotlin · 8 lines
kotlin
sealed interface Result
data class Ok(val value: Int) : Result
data class Err(val msg: String) : Result

fun describe(r: Result): String = when (r) {
    is Ok -> "value=${r.value}"   // smart-cast to Ok
    is Err -> "error=${r.msg}"     // smart-cast to Err
}                                 // exhaustive, no else

go deeper

for a junior

Knows that covering all subtypes lets you drop else and that each branch smart-casts.

for a middle

Explains the maintenance benefit — adding a subtype becomes a compile error rather than a silent runtime fall-through.

for a senior

Distinguishes expression vs statement exhaustiveness and the 1.7 change; argues against defensive else on closed hierarchies.

for a principal

Frames it as compiler-enforced totality over an ADT, a deliberate design lever to make illegal states unrepresentable across a codebase.

## What "sealed" means A `sealed class` or `sealed interface` declares a **closed hierarchy**: the complete set of direct subtypes is fixed and known to the compiler. All subtypes must live in the same module and package (historically the same file). Outside code cannot add new subtypes. ## Expression vs statement - A **`when` expression** is one whose value is *used* — assigned to a variable, returned, or passed as an argument. - A **`when` statement** ignores the result. When a `when` is used as an expression, Kotlin **requires** it to be exhaustive: every possible input must be matched. For arbitrary types you satisfy this with an `else` branch. ## Why no `else` for a sealed type Because the subtype set is closed, the compiler can enumerate every case. If your branches cover all direct subtypes, the `when` is **provably exhaustive** and you may omit `else`. ```kotlin sealed interface Shape data class Circle(val r: Double) : Shape data class Square(val side: Double) : Shape fun area(s: Shape): Double = when (s) { // expression: returned is Circle -> Math.PI * s.r * s.r // s smart-cast to Circle is Square -> s.side * s.side // s smart-cast to Square } // no else needed ``` ## The maintenance payoff If you add `data class Triangle(...) : Shape`, the `when` above **fails to compile** with "`when` expression must be exhaustive". An `else -> error(...)` would have silently swallowed the new case at runtime. So omitting `else` turns adding a variant into a compile-time checklist. ## Statement exhaustiveness (Kotlin 1.7+) Since Kotlin 1.7, a `when` *statement* over a sealed type (or enum/Boolean) also gets an exhaustiveness **warning/error** so you don't lose the check just by ignoring the result. ## Smart-casting Inside each `is` arm the subject is automatically **smart-cast** to that subtype, so you can read its properties without an explicit cast.

  • What happens to this `when` if a teammate adds a new subtype of the sealed interface?
    The `when` expression no longer compiles — it reports it must be exhaustive — forcing you to add a branch for the new subtype.

Like a multiple-choice question where every option is printed: once you tick each option you don't need an 'other' box.

saying these in an interview costs you the question

  • Says you always need an `else` even for sealed types
  • Thinks `else` is preferable for safety (it actually hides new variants)
  • Confuses sealed with open classes
  • Doesn't know exhaustiveness only applies to `when` used as an expression (pre-1.7)

context

open as a page

In a `when` over a sealed type, how does smart-casting work inside each `is` branch, and what can prevent it?

level: middleimportance: must knowfreq 60%

basics

~20 s

Inside an is Subtype -> branch the compiler already knows the value is that subtype, so it auto-casts it and you can use the subtype's properties directly. A mutable var that could change can block this.

open as a page

What is the difference between `when` used as a statement vs an expression over a sealed type with respect to exhaustiveness, and how did Kotlin 1.7 change this?

level: middleimportance: should knowfreq 45%

basics

~20 s

As an expression (its value is used), when over a sealed type must cover all subtypes. As a statement (value ignored), older Kotlin didn't check this; since 1.7 it does and warns/errors if a case is missing.

open as a page

How do exhaustiveness and smart-casting behave for a `when` over a nullable sealed type, and over a sealed hierarchy that itself nests sealed subtypes?

level: seniorimportance: should knowfreq 35%

basics

~20 s

If the value can be null you must add a null branch (or handle it before) to stay exhaustive. For nested sealed types you can either list each leaf or match the intermediate sealed parent in one branch — the compiler tracks both.

open as a page

How do branch guards (`if`/`when` with conditions) interact with exhaustiveness over a sealed type, and when would you deliberately keep or avoid an `else`?

level: seniorimportance: nice to knowfreq 25%

basics

~20 s

If a branch only matches a subtype under an extra condition, the compiler can't prove every value is handled, so you must add another branch or an else. Use else only when you truly accept any other case; otherwise list subtypes so new ones break the build.

open as a page