Exhaustive when

The Compiler Checks Every Case

Because all subclasses are known, a when expression over a sealed type needs no else. If someone adds a new subclass, every when that does not handle it stops compiling, so you can never forget a case.

Example

sealed class PaymentResult {
    data class Success(val txnId: String) : PaymentResult()
    data class Failure(val reason: String, val retryable: Boolean) : PaymentResult()
    data object Pending : PaymentResult()
}

fun message(r: PaymentResult) = when (r) {
    is PaymentResult.Success -> "Paid! Transaction ${r.txnId}"
    is PaymentResult.Failure -> if (r.retryable) "Failed: ${r.reason}. Try again." else "Failed: ${r.reason}"
    PaymentResult.Pending -> "Waiting for bank confirmation..."
}

fun main() {
    listOf(
        PaymentResult.Success("TXN7781"),
        PaymentResult.Failure("Insufficient balance", false),
        PaymentResult.Failure("Bank server timeout", true),
        PaymentResult.Pending,
    ).forEach { println(message(it)) }
}

Output

Paid! Transaction TXN7781
Failed: Insufficient balance
Failed: Bank server timeout. Try again.
Waiting for bank confirmation...

Exercise

Exercise: Model a login result as a sealed class with LoggedIn(userName), WrongPassword(attemptsLeft) and Blocked, and write a when that returns a message for each.

Show answer
sealed class LoginResult {
    data class LoggedIn(val userName: String) : LoginResult()
    data class WrongPassword(val attemptsLeft: Int) : LoginResult()
    data object Blocked : LoginResult()
}

fun msg(r: LoginResult) = when (r) {
    is LoginResult.LoggedIn -> "Welcome ${r.userName}"
    is LoginResult.WrongPassword -> "Wrong password, ${r.attemptsLeft} attempts left"
    LoginResult.Blocked -> "Account blocked"
}