1. Why you need when instead of long if/else if chains
Almost any program that talks to a person (or to another program) sooner or later starts choosing behavior by a “code”: a number, a string, a character. At first everything looks fine: two options—two ifs. But as soon as there are 4–6 options, the else if chain starts stretching downward like a supermarket receipt where you bought “something on sale” and suddenly realized it was a ladle for 69 euros.
The problem is not that if/else if is bad. It’s great. The problem is that a long chain is harder to read: it’s harder for the eye to see “here is the list of all options,” and harder to understand what happens if the value doesn’t match any option. Kotlin gives us a more “table-like” form of branching—when.
Let’s start with a very typical situation: we have a value, and we want to perform different actions for different values.
fun main() {
val role = "editor"
if (role == "viewer") {
println("Access: read-only")
} else if (role == "editor") {
println("Access: editing")
} else {
println("Unknown role: $role")
}
}
The code works. But if there are 6 roles, it will start to look less friendly. That’s where when comes in.
The idea of when: a “mapping table” instead of a “ladder of conditions”
It’s convenient to read when (value) like this: “I have a value value; if it equals this—do that; if it equals something else—do something else; otherwise—do a fallback.” In meaning, it’s very similar to switch from other languages, but in Kotlin the syntax is usually cleaner and more predictable.
So instead of a if/else if/else “ladder,” we get a construct that visually looks like a list of options. And that’s especially nice when you write menus, handle error codes, modes, commands, and other behavior switches.
Let’s rewrite the role example using when:
fun main() {
val role = "editor"
when (role) {
"viewer" -> println("Access: read-only")
"editor" -> println("Access: editing")
else -> println("Unknown role: $role")
}
}
Here you can immediately see “which options are supported” and “what happens otherwise.”
2. when (value) syntax and reading rules
Basic syntax: when (value) { ... }
The syntax of when looks like this: first you write when (value), then curly braces, and inside—branches. Each branch in its basic form looks like “value -> action.”
It’s important to get used to three elements:
- value is what we compare (for example, a menu number or a mode string).
- To the left of -> is “what we compare against.”
- To the right of -> is what to execute if it matches.
A mini-template (almost like a cheat sheet, but legal):
fun main() {
val x = 2
when (x) {
1 -> println("One") // One
2 -> println("Two") // Two
else -> println("Other")
}
}
It looks simple, but behind this simplicity there’s an important rule: checks go top to bottom, and the first matching branch is executed.
else — a “just in case” branch that makes the program more mature
When a person enters data, sometimes they enter the wrong thing. Sometimes on purpose. Sometimes by accident. Sometimes a cat walks across the keyboard—and that’s user input too, just in “cat mode.” That’s why the else branch in when is insurance: “all other values I didn’t list.”
There is an important nuance: if when is used as a statement (i.e., it just performs actions), Kotlin does not require you to handle all cases. Then, if the value doesn’t match any branch, nothing will happen.
However, in learning and practical console programs it’s almost always better to write else explicitly so the user gets a clear response.
An example of “polite” handling of an unknown value:
fun main() {
val day = 9
when (day) {
1 -> println("Mon") // won't run
2 -> println("Tue") // won't run
3 -> println("Wed") // won't run
else -> println("Unknown day: $day") // Unknown day: 9
}
}
It almost always makes more sense to put else at the end—then the “table of options” reads top to bottom, and “otherwise” really means “otherwise.”
A branch as a block: when you need multiple actions in one branch
It often seems that a when branch must be “one action on one line.” In fact, you can do multiple actions in a branch: just put a block { ... } on the right side of -> instead of a single expression.
This is especially useful when you want, for example, to print two messages, or compute something first and then print the result. While we use when as a statement (i.e., “do actions”), this feels very natural.
An example with a “multi-line response”:
fun main() {
val code = 400
when (code) {
200 -> println("OK") // won't run
400 -> {
println("Bad Request") // Bad Request
println("Check the request") // Check the request
}
else -> println("Other code: $code")
}
}
Note: the curly braces inside a branch are not the when braces, but the branch braces—a mini-body where you can write multiple lines.
How a branch is chosen: top-to-bottom order and “first match wins”
It’s very important to understand the branch selection mechanics; otherwise you can accidentally write code that looks logical but works “not like that.” when compares the value with branches in order, top to bottom, and as soon as it finds a match, it executes that branch and exits.
In this lecture we won’t do branches with ranges and conditions (that’s a separate topic), but even in a simple “equality match” it helps to remember: the branch order is part of the logic.
The execution scheme looks roughly like this:
flowchart TD
A["There is a value x"] --> B["Compare with the first branch"]
B -->|matches| C["Execute branch 1 actions and exit"]
B -->|doesn't match| D["Compare with the second branch"]
D -->|matches| E["Execute branch 2 actions and exit"]
D -->|doesn't match| F["..."]
F --> G["If nothing matched → else (if present)"]
From this follows a simple style rule: keep else at the bottom, and list options top to bottom in the order you want people to read them.
3. Practice: the “Wallet” menu and switching to when
Now we’ll do a practical piece that clearly shows the benefit of when: handling a menu item selection. We’ll write a small “wallet”—an application that stores the current balance (just a number) and offers the user a choice of action. We won’t use collections or complex data structures: our goal is to learn how to switch behavior based on a value, not to build a bank.
Print the menu and read the choice
fun main() {
println("=== Wallet ===")
println("1 — show balance")
println("0 — exit")
print("Your choice: ")
val choice = readln().trim()
println("You chose: $choice") // for example: You chose: 1
}
For now the choice is a string. That’s fine: readln() always reads a string, and we prepare it with trim() (remove extra spaces).
Turn input into a number safely
We already know how to do this: toIntOrNull() returns Int?, meaning either a number or null if the user entered something that isn’t a number.
fun main() {
print("Your choice: ")
val choice = readln().trim().toIntOrNull()
if (choice == null) {
println("You need to enter a number.") // You need to enter a number.
return
}
println("You entered: $choice")
}
Here we use an “early exit” (return) so that we only continue with valid data.
The same logic with if/else if, so we have something to compare against
Let’s say we have a balance.
fun main() {
var balance = 100
print("Your choice (1/0): ")
val choice = readln().trim().toIntOrNull() ?: return
if (choice == 1) {
println("Balance: $balance") // Balance: 100
} else if (choice == 0) {
println("Exit") // Exit
} else {
println("Unknown command: $choice")
}
}
It works, but you can already see it’s a “ladder.”
Rewrite it using when (choice)
Now we do the same using when. Essentially, we turn branching into a “table”: “1 → show balance”, “0 → exit”, “otherwise → error”.
fun main() {
var balance = 100
print("Your choice (1/0): ")
val choice = readln().trim().toIntOrNull() ?: return
when (choice) {
1 -> println("Balance: $balance") // Balance: 100
0 -> println("Exit") // Exit
else -> println("Unknown command: $choice")
}
}
In terms of behavior, the program didn’t get “smarter.” But in terms of reading the code, it got calmer: less visual noise, more of a “list of options” feel.
Add actions to feel the scalability
Let’s add “deposit” and “spend” menu items. We’ll keep the operations as simple as possible: read a number and change the balance. This is where when starts to look especially appropriate.
fun main() {
var balance = 100
println("1 — balance, 2 — deposit, 3 — spend, 0 — exit")
print("Your choice: ")
val choice = readln().trim().toIntOrNull() ?: return
when (choice) {
1 -> println("Balance: $balance") // Balance: 100
2 -> { print("Amount: "); balance += readln().trim().toInt() }
3 -> { print("Amount: "); balance -= readln().trim().toInt() }
0 -> println("Exit")
else -> println("Unknown command: $choice")
}
}
Notice that we made branches 2 and 3 blocks { ... } because there are two actions: ask for the amount and change the balance. This “block in a branch” technique is a normal part of when.
Yes, safety can be improved here (for example, protecting against a non-numeric amount), but we’ll do that gradually. Today our goal is to learn how to choose behavior by value in a clean and readable way.
Make the menu “real”: a loop + when
A menu is usually not one-time: the user chooses an action multiple times. We have while, so let’s assemble a simple version that “keeps spinning until we exit.” Inside will be a when that routes actions.
fun main() {
var balance = 100
var running = true
while (running) {
print("1-balance 2+ 3- 0-exit: ")
val choice = readln().trim().toIntOrNull() ?: continue
when (choice) {
1 -> println("Balance: $balance")
2 -> { print("Amount: "); balance += readln().trim().toInt() }
3 -> { print("Amount: "); balance -= readln().trim().toInt() }
0 -> { println("Bye!"); running = false } // Bye!
else -> println("Unknown: $choice")
}
}
}
Note the neat combination: if choice didn’t parse, we do continue and simply retry input. This is not the topic of when, but it’s a typical “skeleton” for console programs: loop + read + branch.
4. Common mistakes when working with when (value)
At the beginning, when often feels “too simple to mess up,” but mistakes still happen—mostly because of readability and habits after if/else. The good news: these mistakes are rarely complex, and you can almost always spot them if you learn to read when as an options table rather than as “another if.”
Mistake #1: forgetting about else and getting “silent behavior.”
Technically, when as a statement does not have to be exhaustive: if no branch matches, nothing will be executed. But in learning tasks and CLI programs this is usually bad UX: the user enters “9” and the program seems to freeze (even though it just did nothing). Most of the time it’s better to explicitly write else and print a clear message.
Mistake #2: placing else not last and hiding the meaning of branches below.
The logic of when is top to bottom: the first matching branch “wins.” If you place else earlier, everything below becomes unreachable or meaningless. Even when the compiler doesn’t complain, the reader gets nervous: “why are there branches if there’s an else above?” Keep else at the end as a style rule.
Mistake #3: bloating branches to 20–30 lines and turning when into a “mini program.”
when is good as a dispatcher: choose an option and run the appropriate action. When half the application appears inside a branch, the “options table” stops being a table and becomes a long canvas. In such cases, discipline helps: keep the branch short, and move details into a separate function (you already know how to write them).
Mistake #4: trying to replace if where there are only two options.
By Kotlin conventions, if is usually preferred for a binary choice, and when when there are three or more options. It’s not a “law,” but it’s a good guideline: if you have “yes/no,” if often reads simpler than when just for one check and an else.
Mistake #5: comparing “dirty input” as-is and being surprised that branches don’t fire.
Today we mostly compared numbers, but even there you can have “dirt” (spaces, an empty string, non-numeric characters). If a value comes in as a string, without normalization (trim(), sometimes lowercase()) you can get “the user entered it correctly, but the program thinks otherwise.” The habit “prepare input first, then branch” saves hours of debugging and slightly preserves your faith in humanity.
GO TO FULL VERSION