skip to content

How do `is`-checks behave inside a `when` expression, and how does that interact with `sealed` exhaustiveness?

level: middleimportance: should knowfreq 65%

answer

  1. is-branch smart-casts the subject
  2. first match wins -> order specific before general
  3. sealed/enum expression -> no else needed
  4. when(val v = ...) makes it stable
  5. avoid else on sealed to keep exhaustiveness

basics

~20 s

In when, each is Type -> branch smart-casts the subject to that type inside the branch. With a sealed class or enum subject used as an expression, the compiler can require all subtypes to be covered, so no else branch is needed.

solid answer

~40 s

A `when (subject)` lets branches match on type with `is Type ->` (and `!is`). Inside each matched branch the subject is smart-cast to that type, so you can access its members directly. Branches are evaluated top-to-bottom; the first match wins, so order more specific types before their supertypes. When `when` is used as an **expression** (its value is consumed) over a **sealed** class/interface or **enum**, the compiler enforces **exhaustiveness**: every subtype/constant must be handled, otherwise it's a compile error — meaning you don't need `else`, and adding a new subtype later surfaces as a compile error at every non-exhaustive `when`. A statement `when` over a non-sealed `Any` still needs `else` if used as an expression. You can mix `is`, value, and range conditions, and combine values with commas.

code

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

fun render(r: Result): String = when (r) { // exhaustive, no else
    is Ok -> r.data
    is Err -> "error ${r.code}"
}

go deeper

for a junior

Knows is branches in when smart-cast and that sealed types can drop else.

for a middle

Explains branch ordering, expression-vs-statement, and the maintenance benefit of exhaustiveness.

for a senior

Uses when (val v = ...) for stability and argues against blanket else on sealed hierarchies.

for a principal

Treats exhaustive when as a design tool for modeling closed domains and reasons about evolution/compatibility when adding subtypes.

## `is` inside `when` A `when` with a subject can branch on type: ```kotlin fun stringify(x: Any): String = when (x) { is Int -> "int ${x + 1}" // x: Int is String -> x.uppercase() // x: String is List<*> -> "list of ${x.size}" else -> "unknown" } ``` Each `is` branch **smart-casts** the subject inside that branch. `!is` works too. Branches are tested in order; the **first** matching branch runs, so place specific types before more general ones (a supertype branch above a subtype branch makes the subtype branch unreachable for that path). ## Smart cast still needs stability The subject of `when` is smart-castable under the same stability rules. `when (val v = expr) { is Foo -> ... }` introduces a stable local `val v`, which is the idiomatic way to make an unstable expression smart-castable. ```kotlin when (val r = repository.find(id)) { is Found -> r.value is Missing -> error("none") } ``` ## Exhaustiveness with `sealed` and `enum` When `when` is an **expression** (its result is assigned/returned) over a `sealed class`/`sealed interface` or an `enum class`, the compiler checks that **all** subtypes/constants are covered. If they are, **`else` is unnecessary**: ```kotlin sealed interface Shape data class Circle(val r: Double) : Shape data class Square(val s: Double) : Shape fun area(sh: Shape): Double = when (sh) { // no else needed is Circle -> Math.PI * sh.r * sh.r is Square -> sh.s * sh.s } ``` If you later add `data class Triangle(...) : Shape`, every exhaustive `when` that omitted `else` now fails to compile, pointing you at each place to update — a major maintenance benefit over a defaulting `else`. ## Statement vs expression - As an **expression** over a sealed/enum subject: exhaustiveness enforced. - As a **statement** (result ignored): historically exhaustiveness was not required, but modern Kotlin warns/errors on non-exhaustive `when` over sealed/enum even as a statement. - Over a plain `Any`: you must provide `else` (the type set is open). ## Avoid `else` for sealed types Adding an `else` branch to a sealed `when` defeats exhaustiveness checking — new subtypes silently fall into `else`. Prefer listing each subtype.

  • Why is adding `else` to a sealed `when` often discouraged?
    It absorbs any future subtype, so the compiler stops flagging unhandled cases; you lose the compile-time safety net that forces you to handle new subtypes.
  • What does `when (val v = expr)` buy you?
    It binds the expression to a stable local `val`, enabling smart casts in branches even when `expr` itself (e.g. a property) isn't smart-castable, and avoids re-evaluating it.

saying these in an interview costs you the question

  • Thinking branch order doesn't matter when a supertype precedes a subtype
  • Claiming `else` is always required even for sealed expressions
  • Not knowing `when` branches smart-cast the subject
  • Believing exhaustiveness works for an open `Any` subject

context