1. When when becomes a problem
If you are thinking right now: “But my when works — why talk about style?”, you are already on the right track. In programming, style is not about aesthetics for their own sake; it is about predictability. You write code once, but you read it (and fix it) many times—sometimes in a state of “I didn’t get enough sleep, the coffee ran out, and it’s crashing.”
when is good because it is obvious to the reader: “here are the options, here are the actions.” But when has three typical “pain points” that beginners run into almost every time:
- branches start to overlap, and the logic depends on the order of lines (a hidden trap);
- else is either forgotten, or used as a trash bin “just in case”;
- branches grow, and when stops being a table and turns into a mini-program inside the program.
To treat these issues, it is important to remember a basic rule: when checks branches from top to bottom and triggers the first matching one. This is not “find the best match,” it is “first one that fits wins.”
2. Overlapping conditions: when line order becomes logic
When you write conditions in when, it’s easy to start thinking: “Well, I listed everything—so everything is covered.” Then an unpleasant surprise appears: if conditions overlap, some branches become unreachable or start triggering not where you expect. And even worse: the code will compile and “seem to work,” but sometimes produce the wrong result.
A classic trap: a wide range “captures” a narrow one
First, let’s make a small “how not to do it” example. It compiles, runs, but the logic tricks us.
fun main() {
val score = 42
val label = when (score) {
in 0..100 -> "Within the normal range"
in 0..50 -> "Rather low"
else -> "Invalid"
}
println(label) // Within the normal range
}
If you expected “Rather low,” that’s normal: you are thinking like a human (“42 fits both, but 0..50 feels more specific”). But when thinks like a machine: the first branch already matched, so we don’t even check the rest.
The correct fix is not magic: we just put the narrower condition above.
fun main() {
val score = 42
val label = when (score) {
in 0..50 -> "Rather low"
in 51..100 -> "Within the normal range"
else -> "Invalid"
}
println(label) // Rather low
}
Here we made the ranges non-overlapping, and branch order stopped being hidden logic.
A “guard” branch with !in: useful, but requires discipline
Validation via !in looks nice: first we filter out clearly invalid values, then we classify the rest. This is a genuinely good pattern—you just need to understand that !in is usually a broad condition, and it should be placed where it won’t break other checks.
fun main() {
val temperature = 999
val message = when (temperature) {
!in -50..60 -> "Outside a reasonable range"
in -50..0 -> "Cold"
in 1..25 -> "Normal"
else -> "Hot"
}
println(message) // Outside a reasonable range
}
Everything is fine here: !in -50..60 does not overlap the other branches because the other branches are entirely inside the -50..60 range.
If you accidentally write the ranges so that the “reasonable” range and the “categories” don’t align, you will get weird behavior. So the practical rule is simple: either make branches non-overlapping, or (if they overlap) put narrower ones above and use the order consciously.
Mini-diagram: how branch order works
Sometimes it helps to literally draw what happens:
flowchart TD
A[Value x] --> B{Does the first branch match?}
B -- yes --> C[Execute the first branch]
B -- no --> D{Does the second branch match?}
D -- yes --> E[Execute the second branch]
D -- no --> F{...}
F -- no --> G["else (if present)"]
The diagram is trivial, but it explains 90% of “why did it behave like that.”
3. else, exhaustiveness, and “silent” when
else is not a “mandatory patch,” but a way to explicitly say: “I accounted for everything else.” The nuance is that the requirement for else depends on whether you use when as an expression or as a statement. This is where common mistakes begin: sometimes the compiler complains, and sometimes it stays silent—and the program “does nothing.”
Kotlin clearly distinguishes when as a statement vs an expression, and expression has an exhaustiveness requirement: you must return a value for any input.
The “it did nothing” bug (when when is a statement)
If when is used as a statement (meaning we do not assign the result to a variable), Kotlin allows you not to cover all cases. This can be convenient, but for a beginner it is often a trap: “I thought it would at least say the command is unknown.”
fun main() {
val cmd = "unknown"
when (cmd) {
"help" -> println("Help")
"exit" -> println("Exit")
}
println("After when") // After when
}
The program “silently” skipped the when. No error. Sometimes that’s fine. But for a command-line interface it’s almost always a bad idea: the user enters a command, and the response is silence. Therefore, in command handling, else is not an option; it is basic politeness.
fun main() {
val cmd = "unknown"
when (cmd) {
"help" -> println("Help")
"exit" -> println("Exit")
else -> println("Unknown command: $cmd") // Unknown command: unknown
}
}
The “doesn’t compile” bug (when when is an expression)
When when returns a value, the compiler requires exhaustiveness—otherwise it cannot guarantee that the variable will get a value. This is not “Kotlin nitpicking,” it is real protection against holes in logic.
fun main() {
val cmd = "help"
// Not allowed: no else, so not all cases are covered
// val message = when (cmd) {
// "help" -> "Help"
// }
val message = when (cmd) {
"help" -> "Help"
else -> "Unknown command: $cmd"
}
println(message) // Help
}
else as diagnostics, not as “anything will do”
A very common mistake is to make else -> "" or else -> 0 just so “the compiler leaves me alone.” Formally, it works, but you disable diagnostics for yourself. In commands, else should help you understand what went wrong: print the original value, say what was expected, and not hide the problem.
4. Long branches: how not to turn when into a mini-program
When you are just learning, it’s tempting to write everything in one branch: parse a number, validate a range, print three lines, update state. This is normal as a first step, but once a branch becomes longer than 10–15 lines, when stops doing its main job: being a table of options.
Kotlin style (and common sense) usually suggests preferring the expression form of when, if, try where it makes the code shorter and clearer. But “shorter” does not mean “in one giant branch.”
Bad style: when does everything at once
Imagine a command grade <number> that prints a letter grade based on points. Here is an example that works but is hard to read.
fun main() {
val arg = "72"
val points = arg.toIntOrNull()
when (points) {
null -> println("Need a number")
else -> {
if (points < 0 || points > 100) {
println("Need 0..100")
} else if (points >= 90) {
println("A")
} else if (points >= 80) {
println("B")
} else {
println("C or lower")
}
}
}
}
Three layers got mixed together: parsing, validation, and business logic. In a small example this is tolerable, but in a real application the branch quickly balloons.
Better: when returns a result, and printing is separate
Let’s do two things: first, when returns a string; second, inside when we avoid nesting if when we can do it more neatly.
fun gradeMessage(points: Int?): String {
return when (points) {
null -> "Need a number"
!in 0..100 -> "Need 0..100"
in 90..100 -> "Grade: A"
in 80..89 -> "Grade: B"
else -> "Grade: C or lower"
}
}
fun main() {
val points = "72".toIntOrNull()
val message = gradeMessage(points)
println(message) // Grade: C or lower
}
We win in several places at once. when became a table. Branches are short. Printing happens once. The logic became testable even “in your head”: you can quickly scan the branches and see the coverage.
Technical nuance: a branch with a block is fine, but the block should be small
Sometimes you really need to do two actions in a branch. That’s acceptable. Just keep the block compact, and if another when or three if statements appear inside, that is a signal to extract the piece into a function.
fun main() {
val cmd = "help"
when (cmd) {
"help" -> {
println("Commands: help, grade <0..100>, exit") // Commands: help, grade <0..100>, exit
println("Example: grade 90") // Example: grade 90
}
else -> println("Unknown command: $cmd")
}
}
5. Choosing the form: if vs when (value) vs when { ... }
This part may seem like preference, but in practice it is a real tool against errors. If you choose the branching form by meaning, the code becomes simpler, and overlapping conditions happen less often. Kotlin conventions explicitly recommend: for binary conditions, if is often better, and when is for three or more options.
if — when there are truly two options
If you have exactly two paths, if is usually more readable. Not because when is “bad,” but because if reads in your head as “either this, or that.”
fun main() {
val raw = " "
val input: String? = if (raw.trim().isEmpty()) null else raw.trim()
println(input) // null
}
You can do it with when too, but it will look like a “two-row table,” which is usually overkill.
when (value) — when comparing to specific values
Commands, codes, statuses—this is the native territory of when (value): it reads like a mapping table.
fun main() {
val role = "editor"
val access = when (role) {
"viewer" -> "Read-only"
"editor" -> "Can edit"
"admin" -> "Can do anything (and even accidentally delete the world)"
else -> "Unknown role: $role"
}
println(access) // Can edit
}
when { ... } — when branches are conditions, not values
The when { ... } form is a great replacement for a chain of if/else if when you classify by conditions. In this form, else is mandatory: without it the compiler will not let you build the code, because otherwise it is unclear what to do if no condition is true.
fun main() {
val x = 0
val sign = when {
x < 0 -> "negative"
x == 0 -> "zero"
else -> "positive"
}
println(sign) // zero
}
Pitfall: when { ... } is easier to “break” with overlaps
When conditions are boolean, overlaps are even easier to create by accident. For example, if you first write x >= 0, and then x == 0, then the branch x == 0 will never trigger.
fun main() {
val x = 0
val label = when {
x >= 0 -> "Non-negative"
x == 0 -> "Zero"
else -> "Negative"
}
println(label) // Non-negative
}
The fix is simple again: put the narrower one higher or make the conditions mutually exclusive.
6. Command-style when: a neat command table
Now we will assemble everything into the very “command dispatcher” you started building in the previous lecture. Our goal is not to add new features, but to show style: normalize input before when, keep branches short, handle unknown cases explicitly, and avoid hiding overlaps in line order.
Step 1: normalize and split input into command and arg
Let’s make a small function that takes an already-trimmed string and returns a pair: command and argument. We use only familiar tools: indexOf and substring.
fun splitCommand(line: String): Pair<String, String> {
val spaceIndex = line.indexOf(' ')
val command = if (spaceIndex == -1) line else line.substring(0, spaceIndex)
val arg = if (spaceIndex == -1) "" else line.substring(spaceIndex + 1).trim()
return command.lowercase() to arg
}
The important thing here: splitCommand does not read input and does not print. It only transforms a string. Such a piece is easy to reuse and hard to “break accidentally.”
Step 2: when as an expression that returns an answer
Now let’s make a handler that returns a response string. This reduces the temptation to print in each branch, and it also helps avoid “sheet” branches: a branch should boil down to computing the answer.
fun handleCommand(command: String, arg: String): String {
return when (command) {
"help", "h", "?" -> "Commands: help, grade <0..100>, echo <text>, exit"
"echo" -> arg
"grade" -> gradeMessage(arg.toIntOrNull())
"exit" -> "EXIT"
else -> "Unknown command: $command"
}
}
Notice the style: we grouped command synonyms with commas to avoid duplicating the same action. This syntax is one of the reasons when is often more readable than an if/else if chain.
Step 3: main becomes a simple “pipeline”
All that’s left is to assemble a small loop. We already know while, so everything is straightforward.
fun main() {
while (true) {
print("> ")
val raw = readln().trim()
if (raw.isEmpty()) {
println("Enter a command. For help: help") // Enter a command. For help: help
continue
}
val (command, arg) = splitCommand(raw)
val response = handleCommand(command, arg)
if (response == "EXIT") break
println(response)
}
}
This is a good example of why we are talking about when style at all. You get a “layer cake” (in a good sense): input → preparation → parsing → dispatching → output. And each place does one job.
A small command pipeline diagram
Sometimes it helps to see it as an assembly line:
flowchart LR
A["readln()"] --> B["trim()"]
B --> C{isEmpty?}
C -- yes --> D[message + continue]
C -- no --> E[splitCommand]
E --> F[handleCommand with when]
F --> G[println/exit]
And yes: if you felt like a factory engineer—congratulations, you just took your first step toward architecture.
When when starts to grow
In real life, there are more commands. Branches grow. And one day your neat when (command) turns into an 80-line canvas where half the branches also repeat the same thing. This is not “bad,” it is natural growth. The key is to notice in time when when stopped being a table.
Common signs: nested if appears inside a branch, repeated parsing like arg.toIntOrNull(), identical error messages, and you start scrolling the when with the mouse wheel.
The simplest way to contain growth is not to try to “defeat” when, but to help it: extract repeated parts into functions and prepare data ahead of dispatching. For example, if half the commands work with a number, you can make one argument-parsing function that returns Int?, and then use its result in when. This is not a new topic—it is normal decomposition that you have already done with functions.
7. Common mistakes when working with when style
Mistake #1: overlapping conditions where branch order becomes hidden logic.
The most insidious situation is when code compiles and “often works,” but a lower branch never triggers because a broader range or condition is above it. This is fixed either by rearranging branches (narrow above wide) or by making conditions mutually exclusive so you stop depending on order.
Mistake #2: else is used as a “trash bin” so the compiler stops complaining.
When when is an expression, the compiler requires exhaustiveness. Beginners sometimes put else -> "" or else -> 0. Formally the program builds, but you lose diagnostics: the error turns into a quiet wrong result. It is better to make else meaningful: report an unknown value, show the original input.
Mistake #3: when without else in commands becomes “silence” for the user.
When when is used as a statement, Kotlin allows not covering all cases, and if nothing matches—nothing happens. For a command-line interface this is almost always bad UX: a person entered a command and didn’t understand what happened. Therefore, in command handling, else is a mandatory “unknown command” branch.
Mistake #4: branches become too long, and when stops being a table.
As soon as a branch contains parsing, validation, several println calls, nested if, and another when, readability drops sharply. The right habit is to return a result (string/status) from when, and extract details into functions. This makes the code linear and helps keep branches short.
Mistake #5: choosing when where if is simpler, and vice versa.
If there are exactly two options, if is usually faster to read. Kotlin conventions explicitly advise preferring if for binary conditions and when for three or more options. The opposite mistake is writing a long if/else if chain where the meaning is a “table of values”: there, when (value) makes the code more uniform and reduces duplication.
Mistake #6: input normalization is done inside branches instead of before when.
If in each branch you write raw.trim().lowercase() or do substring() directly in when conditions, then your when stops being a dispatcher: it becomes “the place where we do everything at once.” It is much more reliable to compute command and arg once, and then in when work with already-clean values.
GO TO FULL VERSION