when Expressions

A when expression is Kotlin’s replacement for both Java’s switch statement and long if/else if chains. It compares a value against a list of branches and executes (or evaluates to) whatever the first matching branch specifies. Unlike a Java switch, a Kotlin when can match on any type, can run without a subject at all to test arbitrary boolean conditions, and — most importantly — can be used as an expression that produces a value, with the compiler forcing you to handle every possible case.

Overview / How when Works

when has two shapes. As a statement, it works like a value-based dispatcher: Kotlin evaluates the subject once, checks each branch from top to bottom, and runs the code next to the first branch whose condition matches. There is no fall-through between branches the way there is in Java’s switch — you never need a break, and forgetting one can never leak execution into the next branch. As an expression, when instead evaluates to the value produced by the matching branch, and that value can be assigned to a val, returned from a function, or passed as an argument.

When when has a subject — when (x) { ... } — each branch is compared against x using structural equality, the same == operator that calls equals(). This is why when works uniformly on strings, data classes, enums, and any other type, not just the primitives and enum constants that Java’s switch is restricted to. When when has no subject — when { ... } — each branch is instead an independent boolean expression, and the first one that evaluates to true wins. This form is effectively a tidier if/else if chain.

The compiler enforces exhaustiveness only when the result of when is actually used as a value — assigned to a variable, returned, or passed to another function. In that case every possible input must be handled, either with an explicit else branch or, for a sealed class hierarchy or an enum class, by covering every subtype or entry (the compiler knows the complete, closed set of possibilities and can verify this for you at compile time). If you use when purely as a statement and discard its result, exhaustiveness is not required — an unmatched value simply falls through and nothing happens.

Branches can also do more than test plain equality: in 1..10 tests range or collection membership, is String tests (and smart-casts to) a type, and comma-separated values like 1, 2, 3 -> match any of several values with one branch. Inside an is Type -> branch, the compiler automatically treats the subject as that type for the rest of the branch — no manual cast is needed, the same smart-casting behavior you get from an if (x is Type) check.

Syntax

With a subject, each branch is compared against it:

when (subject) {
    value -> // result if subject == value
    valueA, valueB -> // result if subject equals either
    in range -> // result if subject is in the range
    is Type -> // result if subject is that type (smart-cast inside)
    else -> // result if nothing above matched
}

Without a subject, each branch is its own boolean condition:

when {
    booleanExpression1 -> // result if true
    booleanExpression2 -> // result if true
    else -> // result if nothing above was true
}
Part Meaning
when (subject) Optional value compared against each branch with equals(). Omit it to write boolean conditions instead.
condition -> result One branch: a value, comma-separated values, an in range/collection check, an is type check, or (with no subject) a boolean expression, followed by the code to run or value to produce.
else -> result Fallback branch. Required whenever when is used as an expression and the compiler can’t already prove every case is covered.
result A single expression, or a { } block whose last line is the produced value (when when is used as an expression).

Examples

Example 1: when as a statement

The simplest use of when mirrors a Java switch: pick a branch based on a subject and run some code. Comma-separated values let one branch handle several cases at once, and there is no fall-through to guard against.

fun main() {
    val day = 3
    when (day) {
        1 -> println("Monday")
        2 -> println("Tuesday")
        3 -> println("Wednesday")
        4 -> println("Thursday")
        5 -> println("Friday")
        6, 7 -> println("Weekend")
        else -> println("Invalid day")
    }
}

Output:

Wednesday

Kotlin evaluates day once, checks each branch in order, and finds a match at 3 -> println("Wednesday"). The 6, 7 -> branch shows how to group multiple values behind a single arrow. Because this when is used as a statement — its result is never assigned to anything — the else branch is here only for completeness; it would not be required for the code to compile.

Example 2: when as an expression with ranges

Assigning the result of when to a val switches it into expression form. Combined with in range checks, this replaces a long chain of comparisons cleanly.

fun main() {
    val score = 82
    val grade = when (score) {
        in 90..100 -> "A"
        in 80..89 -> "B"
        in 70..79 -> "C"
        in 60..69 -> "D"
        else -> "F"
    }
    println("Score $score -> Grade $grade")
}

Output:

Score 82 -> Grade B

Because grade is assigned from the result of when, this is the expression form, so the compiler requires every possible Int to be handled — that’s why else -> "F" is mandatory here; Int has far too many values to enumerate. 82 falls inside 80..89, so grade becomes "B".

Example 3: when without a subject

Dropping the subject turns each branch into an independent boolean condition, useful when the cases don’t all compare the same value the same way.

fun main() {
    val temperature = -5

    val description = when {
        temperature < 0 -> "freezing"
        temperature in 0..15 -> "cold"
        temperature in 16..25 -> "mild"
        else -> "hot"
    }
    println("It's $description outside ($temperature°C).")
}

Output:

It's freezing outside (-5°C).

temperature < 0 is the first condition that is true, so "freezing" is chosen immediately; the later in 0..15 branch is never even evaluated. This form is essentially a more readable if/else if chain, and it still requires else because the compiler can’t prove the boolean conditions cover every case.

Example 4: when with sealed classes and smart casting

The most powerful use of when pairs it with a sealed class. Because a sealed class’s subtypes are all known and declared together, the compiler can verify a when over it is exhaustive without needing an else at all.

sealed class Shape
data class Circle(val radius: Double) : Shape()
data class Rectangle(val width: Double, val height: Double) : Shape()
data class Triangle(val base: Double, val height: Double) : Shape()

fun area(shape: Shape): Double = when (shape) {
    is Circle -> Math.PI * shape.radius * shape.radius
    is Rectangle -> shape.width * shape.height
    is Triangle -> 0.5 * shape.base * shape.height
}

fun main() {
    val shapes = listOf(Circle(2.0), Rectangle(3.0, 4.0), Triangle(6.0, 5.0))
    for (shape in shapes) {
        println("Area: ${"%.2f".format(area(shape))}")
    }
}

Output:

Area: 12.57
Area: 12.00
Area: 15.00

Each is Circle ->, is Rectangle ->, and is Triangle -> branch both tests the runtime type of shape and smart-casts it, so shape.radius is directly accessible inside the Circle branch without any manual casting. Since Shape is sealed and every one of its three subtypes is handled, the compiler considers the when exhaustive on its own — no else branch is written or needed.

How it works step by step

Walking through Example 4: first, shapes is built as a List<Shape> holding one instance of each subtype. The loop then passes each element to area(). Inside area(), the single-expression function body is exactly the value produced by when (shape) { ... }. For each call, Kotlin checks is Circle first: if the runtime type matches, that branch’s expression is evaluated and immediately becomes the result of the whole when, and therefore the return value of area() — no further branches are checked. If it doesn’t match, evaluation moves to is Rectangle, then is Triangle, in source order. Because Shape is sealed, one of the three is guaranteed to match, which is exactly what lets the compiler skip requiring an else. The formatted area is then interpolated into a string and printed once per shape.

Common Mistakes

Mistake 1: forgetting that an expression when must be exhaustive

This looks reasonable but does not compile, because describe‘s body is a when expression over an Int, and Int has far more possible values than 1 and 2:

fun describe(x: Int): String = when (x) {
    1 -> "one"
    2 -> "two"
}

The compiler rejects this with an error along the lines of “‘when’ expression must be exhaustive, add necessary ‘else’ branch”, because it cannot prove every Int is covered. Adding else fixes it:

fun describe(x: Int): String = when (x) {
    1 -> "one"
    2 -> "two"
    else -> "other"
}

fun main() {
    println(describe(5))
}

Output:

other

Mistake 2: ordering boolean branches from general to specific

Branches without a subject are checked top to bottom, and the first one that is true wins — even if a later branch would also match and be more accurate. This compiles fine but gives a misleading answer:

fun main() {
    val n = 5
    val category = when {
        n > 0 -> "positive"
        n > 3 -> "big positive"
        else -> "non-positive"
    }
    println(category)
}

Output:

positive

Since 5 satisfies n > 0, that branch matches first and n > 3 is never reached, even though it would have been the more specific, presumably intended answer. The fix is to order conditions from most specific to least specific:

fun main() {
    val n = 5
    val category = when {
        n > 3 -> "big positive"
        n > 0 -> "positive"
        else -> "non-positive"
    }
    println(category)
}

Output:

big positive

Best Practices

  • Prefer when over chained if/else if once you have three or more branches on related conditions — it reads more clearly and the compiler can check it for completeness.
  • Use when as an expression (assign or return its result) rather than assigning inside every branch — it keeps “one value, one place it’s set” and unlocks exhaustiveness checking.
  • Model closed sets of cases with a sealed class or enum class and omit else — if someone adds a new subtype or entry later, every non-exhaustive when over it will fail to compile until it’s updated.
  • Order boolean-condition branches from most specific to least specific; the first match wins, so a broad condition placed first silently shadows a narrower one below it.
  • Use in for ranges and collections and is for type checks instead of writing raw comparisons — they document intent and read closer to natural language.
  • Keep branch bodies short; if one needs several statements, wrap it in { } with the last line as the value, but consider extracting a function if it grows past a few lines.

Practice Exercises

  1. Write fun dayType(day: Int): String that returns "Weekday" for 1..5, "Weekend" for 6 and 7, and "Invalid" otherwise, using when with ranges. Test it with a few values from main().
  2. Define a sealed class TrafficLight with three objects, Red, Yellow, and Green. Write a function that returns the action to take ("Stop", "Prepare", "Go") using a when expression over TrafficLight with no else branch.
  3. Given a val input: Any that might hold an Int, a String, or a Boolean, write a when (input) { is Int -> ... } chain that prints a different message per type, plus an else branch for anything else. Try it with a few different values.

Summary

  • when replaces switch and if/else if chains; branches are checked top to bottom and the first match wins, with no fall-through.
  • when (subject) { ... } compares using structural equality (equals()); when { ... } with no subject evaluates independent boolean conditions instead.
  • Used as a statement, when needs no else; used as an expression, it must be exhaustive — supply else, or cover every subtype of a sealed class / every entry of an enum class.
  • Branches support comma-separated values, in for ranges and collections, and is for type checks with automatic smart-casting.
  • Order matters for boolean-condition branches: put more specific conditions before more general ones, since the first true branch always wins.