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
whenover chainedif/else ifonce you have three or more branches on related conditions — it reads more clearly and the compiler can check it for completeness. - Use
whenas 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 classorenum classand omitelse— if someone adds a new subtype or entry later, every non-exhaustivewhenover 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
infor ranges and collections andisfor 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
- Write
fun dayType(day: Int): Stringthat returns"Weekday"for1..5,"Weekend"for6and7, and"Invalid"otherwise, usingwhenwith ranges. Test it with a few values frommain(). - Define a
sealed class TrafficLightwith three objects,Red,Yellow, andGreen. Write a function that returns the action to take ("Stop","Prepare","Go") using awhenexpression overTrafficLightwith noelsebranch. - Given a
val input: Anythat might hold anInt, aString, or aBoolean, write awhen (input) { is Int -> ... }chain that prints a different message per type, plus anelsebranch for anything else. Try it with a few different values.
Summary
whenreplacesswitchandif/else ifchains; 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,
whenneeds noelse; used as an expression, it must be exhaustive — supplyelse, or cover every subtype of asealed class/ every entry of anenum class. - Branches support comma-separated values,
infor ranges and collections, andisfor 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.
