1. Why use when as an expression
When we write console programs, it’s very easy to slip into the style of “a println(...) in every branch.” It works… while the program is small. But as soon as you want to first compute the message text and then decide where to send it (to the console, to a log, to a file), or you want to return a result from a function—you suddenly need a when that returns a value, not just performs an action.
In Kotlin, when can be both a statement (statement) and an expression (expression). The difference is that an expression produces a result that can be assigned to a variable or returned from a function. Kotlin explicitly emphasizes this idea: when as an expression returns a value, while as a statement it just runs code with no result.
Mini-parallel: statement vs expression
| What we want | How we write it | What we get |
|---|---|---|
| “Do an action” | |
We return nothing; we just execute |
| “Return a value” | |
We get a value and use it later |
This idea is described exactly like this in the official explanation of when as an expression vs a statement.
2. Basic syntax: val result = when (...) { ... }
Now we’ll take the most important step of the day: stop printing inside when (though sometimes you can), and start getting a string/number as the result. This is similar to if as an expression that you’ve already seen: instead of “branching does something,” we move to “branching chooses a value.”
The key form looks like this:
val message = when (x) {
1 -> "..."
2 -> "..."
else -> "..."
}
Here message is a normal variable, and when (...) {...} is an expression that “evaluates” to a single value.
Example 1: choose a message by status code
fun main() {
val code = 404
val message = when (code) {
200 -> "OK"
404 -> "Not Found"
else -> "Other code: $code"
}
println(message) // Not Found
}
Notice: we don’t print in the branches. We return strings, and we print once—after when. This immediately imposes order: “first decide what to say, then say it.”
3. Exhaustiveness and else
Now the main “freeze-frame” of this lecture. When when is used as a statement (statement), Kotlin allows you not to cover all cases: if no branch matches, nothing will happen. The official example demonstrates exactly this behavior: not all cases are covered, no branch is selected, but there is no error.
But when when is used as an expression, Kotlin requires exhaustiveness: an expression must have a result for any input. Otherwise, what should be assigned to the variable? “Nothing”? The compiler disagrees.
Kotlin states it directly: if when is used as an expression, you must cover all cases, otherwise you’ll get a compilation error.
What the error looks like in real life
Imagine you wrote this:
fun main() {
val code = 500
val message = when (code) {
200 -> "OK"
404 -> "Not Found"
// no else
}
println(message)
}
The logic is clear: “for now I only care about 200 and 404.” But the compiler will say something like: “what should I do if code is 500?”—and it will be right. That’s why else most often appears in an expression-style when.
“else is not required”—but don’t celebrate yet
In theory, you can omit else if you have guaranteed that you listed all possible options. Kotlin has types where this is realistic (for example, for some closed sets of values), but then types appear that we’ll cover in detail later in the course.
At the current level, it’s easier to remember the honest beginner rule: if when returns a value, you always need else.
4. Branch value: -> and a { ... } block
When a when branch is a single line after ->, it’s simple: that’s the value. But pretty quickly you’ll want to do a couple of actions inside a branch: for example, prepare text, normalize data a bit, or compute something step by step. Then the branch becomes a { ... } block.
And here it’s important to know the rule: the value of a block-branch is the last expression inside the block. Kotlin emphasizes that a branch can be a block, and its value will be the value of the last expression in that block.
Example 2: a block-branch returns the last expression
fun main() {
val code = 400
val message = when (code) {
200 -> "OK"
400 -> {
val base = "Bad Request"
"$base (check the input data)"
}
else -> "Other code: $code"
}
println(message) // Bad Request (check the input data)
}
Here we prepared the base variable inside the branch, and returned a template string on the last line.
A nuance that saves hours of your life
If the last line in a branch is println(...), the branch returns Unit, and then the “fun” begins: different branches return different types. So when you use when as an expression, try to keep branches returning values, and do printing outside.
5. Result type of when: all branches must agree
When when is an expression, it has a single result, so it has a single type. Kotlin will try to infer a common type for all branches. If one branch returns String, another Int, and a third returns Unit, then the common type becomes too “wide” (for example, Any), and it will be inconvenient to use later.
Instead of a technical formulation, remember the practical one: decide in advance what your when returns. If it’s a message—let all branches return String. If it’s a number—let them return a number.
Example 3: when returns a number
fun main() {
val month = 2
val daysInMonth = when (month) {
1, 3, 5, 7, 8, 10, 12 -> 31
4, 6, 9, 11 -> 30
2 -> 28
else -> 0
}
println(daysInMonth) // 28
}
Yes, here we used grouping of values with commas—this is legal and readable, and Kotlin presents this style as a standard technique for when.
And else -> 0 here is not “just for looks”: it covers the case where “the user entered 13.” We’re not validating ranges nicely yet—that will be the next lecture—but we need exhaustiveness already.
6. Practice: when in functions, with null, and without else
In real programs, branching often lives inside functions: “get a code—return text,” “get a command—return a hint.” Kotlin even recommends in its style conventions using the expression form of if/when/try where you return a value, because it makes the code shorter and more straightforward.
Next are several typical situations where when as an expression is especially good.
when + functions: return a value without if “spaghetti”
Let’s make a small function for our future console app (in the next lectures of the day, we’ll start handling commands). For now, no input—just to see the architecture: the function returns a string.
fun statusText(code: Int): String = when (code) {
200 -> "OK"
400 -> "Bad Request"
404 -> "Not Found"
else -> "Unknown: $code"
}
fun main() {
println(statusText(404)) // Not Found
}
This reads almost like a dictionary: “200 is OK, 400 is Bad Request…”. And the main thing: the function always returns a string, because when is exhaustive.
Nullable values in when: a separate null branch
Now a very real-life story: the user entered something, you tried to parse it, got an Int? (that is, “a number or null”), and then you need to respond nicely.
when is especially convenient here because null can be handled with a separate branch. This naturally fits with our previous experience with toIntOrNull() and basic null-safety.
fun main() {
print("Enter age: ")
val raw = readln().trim()
val age: Int? = raw.toIntOrNull()
val message = when (age) {
null -> "This doesn't look like a number: \"$raw\""
0 -> "Age 0 sounds like the start of a programmer's career."
else -> "Your age: $age"
}
println(message)
}
The important thing here is that message is a string in any scenario. Even if age == null, we still return a string, so when is exhaustive.
Mini-refactor of a “console helper”: first build the reply, then print
Now we’ll start building a style that will be especially useful in the lecture about commands. Imagine we have a “console helper”—an app that reacts to a command and outputs text. For now without readln() and normalization (that will be the next level’s lecture), just a sketch.
The key idea of this lecture: branching returns a string, and printing is done once.
fun replyFor(command: String): String = when (command) {
"help" -> "Available commands: help, exit"
"exit" -> "Shutting down..."
else -> "Unknown command: $command"
}
fun main() {
val cmd = "help"
val reply = replyFor(cmd)
println(reply) // Available commands: help, exit
}
See how convenient this is: replyFor(...) can be reused later. In the next lecture, you’ll learn how to “extract” command from user input, normalize with trim().lowercase(), and then when will become a real command dispatcher. But the foundation is exactly here: when as an expression returns a result.
when without else: an example with Boolean
It’s useful to see a case where else truly isn’t needed, to understand the idea of exhaustiveness rather than just memorizing “write else.”
A Boolean has only two values, and if you handled true and false, then you handled everything. In this case, when is exhaustive without else.
fun main() {
val isAdmin = true
val access = when (isAdmin) {
true -> "Access: full"
false -> "Access: limited"
}
println(access) // Access: full
}
Why do I still not recommend fanatically avoiding else? Because as soon as nullable logic appears (Boolean?) or you make a more complex type, “everything is covered” suddenly stops being obvious. At your current stage, else is not an enemy but a safety net against surprises from the compiler and the user.
7. Common mistakes when using when as an expression
Mistake No. 1: forget else and be surprised why “the compiler suddenly complains.”
This is the most common situation: you wrote nice branching, assigned it to a val, and expect the program to just “do nothing” if nothing matches. But an expression must return a value, so else (or a complete set of cases) is required. This is not Kotlin being picky—it’s logic: otherwise it’s unclear what to assign.
Mistake No. 2: return different types from different branches and end up with a weird common type.
For example, return "OK" in one branch, 0 in another, and call println(...) in a third. Kotlin will start looking for a common type, and you’ll get a result that is inconvenient to work with. Good style is to choose the when “contract” in advance: return a string or return a number—and stick to it in all branches.
Mistake No. 3: print (println) inside branches and then try to use the result.
This is the trap of “I’m used to if/else where I did an action.” If there’s a println(...) inside a branch, that’s an action, not a value. As a result, you either won’t be able to assign the result, or you’ll get Unit. Practical technique: first compute val message = when (...) { ... }, and then do a single println(message) outside.
Mistake No. 4: make block-branches but forget that the last expression is returned.
Sometimes a person writes a block inside a branch, creates variables, and accidentally leaves “the wrong thing” as the last line (for example, a function call that returns Unit). Then the branch returns Unit, and the result type of when breaks. If a branch is a block, ask yourself: “what is the last line and what does it return?” Kotlin explicitly describes the rule “the last expression is the branch value.”
Mistake No. 5: mix everything inside when at once: computations, input, printing, and error handling.
Even if it works, such code is hard to read: when turns into a small “program inside a program.” Keep branches short: let when answer “which result should I choose?”, and let the other actions (input/printing) live nearby and be clearly separate. This style is one of the reasons Kotlin’s coding conventions prefer the expression form of conditional constructs: it makes the logic direct.