JavaRush /Courses /Kotlin SELF /when with enum — exhaustive branches and when as an expre...

when with enum — exhaustive branches and when as an expression

Kotlin SELF
Level 34, Lesson 1
Available

1. Introduction

When you start using enum in real code, a very practical question comes up quickly: “OK, I have ExpenseCategory.FOOD—what do I do with it next?” Display a nice name? Choose a limit? Pick a hint for the user? Put an “icon” as text? And this is where people often slide into if statements or “magic strings”. when is exactly the tool that turns handling enum variants into a clear mapping table.

Imagine enum as a set of buttons on a remote: there aren’t many buttons, they’re labeled, and the list is known in advance. when is your instruction: “if this button is pressed—do this”. And if you later add a new button, Kotlin can remind you: “Hey, you didn’t write an instruction for the new button”.

2. Remembering when: statement vs expression

In words, the difference sounds pedantic: a statement “just executes code”, an expression “returns a value”. But in practice it literally determines how short and readable your code will be. When when is used as an expression, we often get rid of temporary variables and repeated return. The code starts to look like a neat table: “here’s input → here’s output”.

In Kotlin, when can be written either way. And the nicest part: a when expression can be assigned to a variable or returned from a function. This is especially natural for enum: input is one of a fixed set of options, output is, for example, UI text or a number for calculations.

Mini example: when as a statement

enum class Status { NEW, IN_PROGRESS, DONE }

fun printStatus(status: Status) {
    when (status) {
        Status.NEW -> println("Created")              // Created
        Status.IN_PROGRESS -> println("In progress")     // In progress
        Status.DONE -> println("Done")              // Done
    }
}

Here when simply executes println() in the appropriate branch.

Mini example: when as an expression

enum class Status { NEW, IN_PROGRESS, DONE }

fun statusLabel(status: Status): String =
    when (status) {
        Status.NEW -> "Created"
        Status.IN_PROGRESS -> "In progress"
        Status.DONE -> "Done"
    }

Notice: the branches return strings, and the entire when returns a string. No var label = ... and no three returns.

3. Exhaustive when for enum

Now comes one of the nicest moments in Kotlin: when when works with an enum, the compiler knows the full list of variants. That means it can verify that you handled every variant. This is called an exhaustive when (exhaustive).

Psychologically, it feels like “I didn’t forget a case”. Technically, it’s protection against future changes. If you add a new enum entry, Kotlin will highlight places where handling became incomplete.

Let’s make a small piece for our training console app (roughly “expense tracking”). Suppose we have expense categories:

enum class ExpenseCategory { FOOD, TRANSPORT, HOME, FUN }

And we want to display the category name nicely in English.

enum class ExpenseCategory { FOOD, TRANSPORT, HOME, FUN }

fun categoryTitle(category: ExpenseCategory): String =
    when (category) {
        ExpenseCategory.FOOD -> "Food"
        ExpenseCategory.TRANSPORT -> "Transport"
        ExpenseCategory.HOME -> "Home"
        ExpenseCategory.FUN -> "Entertainment"
    }

There is no else here—and that’s good. Why? Because if tomorrow you add HEALTH, the compiler will say: “Aha, categoryTitle() no longer covers all variants”. And you’ll fix it not “sometime later”, but right away.

To feel the idea, you can imagine when over an enum as a strict report form: until you fill in all the rows, the report isn’t accepted.

4. Why else in when over enum is often harmful

Sometimes your hand reaches for:

else -> "Unknown"

The problem is that for an enum, “unknown” most often means: “I added a new option, and old code silently swallowed the change and did something weird”. Yes, it compiles. But the price is losing automatic completeness checking.

There are situations where else is still appropriate—for example, if you consciously don’t want to distinguish variants and you implement a default behavior. But for “mapping table” logic, it’s almost always better to list everything explicitly.

To make the comparison clear, here’s a small table:

Approach What we gain What we lose
when over enum without else The compiler checks completeness; easier to maintain You must actually handle every variant
when over enum with else Faster to “throw together”; sometimes handy for a prototype The compiler stops helping when the enum is extended

5. Result type of a when expression

When when is an expression, Kotlin must understand which type it returns. Usually it’s simple: all branches return String—so when returns String. But beginners are sometimes surprised when branches return different types and the compiler starts “looking for a common type”.

For simplicity, keep this rule: all branches of a when expression should return values of the same type (or at least compatible ones). For an enum this is almost always natural: you produce either strings, numbers, or some specific objects.

Example: when returns a number

enum class ExpenseCategory { FOOD, TRANSPORT, HOME, FUN }

fun defaultLimit(category: ExpenseCategory): Int =
    when (category) {
        ExpenseCategory.FOOD -> 1500
        ExpenseCategory.TRANSPORT -> 800
        ExpenseCategory.HOME -> 2000
        ExpenseCategory.FUN -> 1000
    }

Example: the common type becomes too broad

enum class ExpenseCategory { FOOD, TRANSPORT }

fun weird(category: ExpenseCategory) =
    when (category) {
        ExpenseCategory.FOOD -> "food"
        ExpenseCategory.TRANSPORT -> 123
    }

You can write it this way, and Kotlin will infer a common type (most likely Any). But you’ll make your life harder: instead of a clear String or Int you’ll get Any, and you’ll have to figure out what’s inside. In a real project, such a when is almost always a signal: “stop—you mixed two different tasks”.

6. Nullable enum (E?): add a null branch

In apps, a common situation is “the value hasn’t been chosen yet”. For example, an expense category isn’t specified because the user entered only the amount. Then the type can be ExpenseCategory?.

And here is an important rule: if you do when on a nullable enum and you want an exhaustive when, you need to handle null as a separate branch.

enum class ExpenseCategory { FOOD, TRANSPORT, HOME, FUN }

fun categoryTitleOrUnknown(category: ExpenseCategory?): String =
    when (category) {
        ExpenseCategory.FOOD -> "Food"
        ExpenseCategory.TRANSPORT -> "Transport"
        ExpenseCategory.HOME -> "Home"
        ExpenseCategory.FUN -> "Entertainment"
        null -> "No category"
    }

This looks a bit longer, but it’s honest. And it reads very transparently: “if null, then no category”.

7. Example: when over enum in a console app

Now let’s assemble a mini piece of logic that really shows up in CLI programs: user commands. Before, we could compare strings ("add", "list", "exit"), but strings are easy to mistype and hard to refactor. enum makes the list of commands explicit.

Declare the command enum

enum class Command { ADD, LIST, HELP, EXIT }

when as an expression: produce help text

enum class Command { ADD, LIST, HELP, EXIT }

fun commandHelp(command: Command): String =
    when (command) {
        Command.ADD -> "add — add an expense"
        Command.LIST -> "list — show all expenses"
        Command.HELP -> "help — show help"
        Command.EXIT -> "exit — quit the program"
    }

This is a classic “mapping table” case: enum → text.

when as a statement: perform an action

For now, let the actions be simple (we’re not complicating the app here; we’re only demonstrating the style).

enum class Command { ADD, LIST, HELP, EXIT }

fun runCommand(command: Command) {
    when (command) {
        Command.ADD -> println("Adding...")   // Adding...
        Command.LIST -> println("List...")      // List...
        Command.HELP -> println("Help...")     // Help...
        Command.EXIT -> println("Exiting...")       // Exiting...
    }
}

Notice: this is also exhaustive, and else is not needed.

How a when expression removes duplication

A typical beginner problem is repeating the same thing in every branch. For example, you want to print the result the same way: println(result). Then it’s convenient to first get a string from when, and then print once.

enum class Command { ADD, LIST, HELP, EXIT }

fun runCommandMessage(command: Command): String =
    when (command) {
        Command.ADD -> "OK, adding an expense"
        Command.LIST -> "Showing the list of expenses"
        Command.HELP -> "Here is help for the commands"
        Command.EXIT -> "Bye! (but the code is still running)"
    }

fun main() {
    val cmd = Command.HELP
    println(runCommandMessage(cmd)) // Here is help for the commands
}

Here you clearly separated responsibilities: when chooses the text, and println performs the output. Plus it’s easier to test in your head.

A small diagram: when over enum as controlled branching

Sometimes it helps to see that when over an enum is literally a fork with fixed roads. Here’s a flowchart of what a when expression does (at the idea level):

flowchart TD
    A[Got an enum value] --> B{"when(enum)"}
    B -->|Option 1| C[Result/action #1]
    B -->|Option 2| D[Result/action #2]
    B -->|Option 3| E[Result/action #3]
    B -->|Option N| F[Result/action #N]

If an enum has a fixed set of options, then the compiler can verify that all “roads” exist. And if you later add a new road, the compiler will require you to add handling—so the user doesn’t end up in the bushes.

8. Typical mistakes when using when with enum

Mistake #1: adding else “just in case” and losing completeness checking.
At first, else feels like a safety net. But for an enum it often turns into a maintenance trap: you expanded the enum, and old code kept compiling and behaving differently than intended. If you truly want a default, it’s better to name it clearly: sometimes it’s a separate enum variant (for example, UNKNOWN), not a silent else.

Mistake #2: using when as an expression but returning different types from branches.
Kotlin will infer a common type, and you’ll suddenly get Any, after which strange checks and casts begin. In training tasks this looks like “well, it works”, but in real code it turns into a confusing contract: it’s unclear what the function returns and how to use it.

Mistake #3: writing too much code inside when branches.
when reads great when branches are short: return a string, return a number, call one clear function. When a branch contains five printlns, a loop, and another when, your brain starts asking for a vacation. Good style: when chooses what to do, and complex logic lives in separate functions.

Mistake #4: forgetting about null when the enum is nullable (E?).
If you switched to ExpenseCategory?, but your when doesn’t account for null, then you’ll either have to write else, or you’ll end up with a non-exhaustive when expression (and the compiler will start complaining if you want to return a value). In Kotlin, null is just another option like the rest—it just requires its own branch.

Mistake #5: mixing “choosing a value” and “printing to the screen” in each branch.
Beginners often do this: in each when branch they print something, and then they print something common again. As a result, duplication grows. It’s usually simpler to make when an expression, get a string, and then call println once. That makes the code linear and predictable.

1
Task
Kotlin SELF, level 34, lesson 1
Locked
Order badge
Order badge
1
Task
Kotlin SELF, level 34, lesson 1
Locked
Device limit
Device limit
1
Task
Kotlin SELF, level 34, lesson 1
Locked
Delivery method not selected
Delivery method not selected
1
Task
Kotlin SELF, level 34, lesson 1
Locked
School schedule
School schedule
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION