Conditional Statements #

Every useful program needs to make decisions: do this if a condition holds, do that if it doesn’t. In almost every programming language, these decisions are made with if and switch. Kotlin takes the same concept but pushes it further: if and when aren’t just statements — they’re expressions that return values. This isn’t just a small technical detail. Being able to use control structures as expressions makes code more declarative, easier to read, and reduces the need for temporary variables. This article covers how to use if and when idiomatically in Kotlin — from the most basic forms to the patterns that make code more expressive.

if as a Statement #

The most basic form of if works exactly like in Java or other languages: execute a code block if the condition holds.

val temperature = 38.5

if (temperature >= 37.5) {
    println("Fever — rest and drink water")
}

Add else to handle conditions that aren’t met:

val balance = 150_000
val shoppingTotal = 200_000

if (balance >= shoppingTotal) {
    println("Payment successful")
} else {
    println("Insufficient balance. Short by: ${shoppingTotal - balance}")
}

And else if for chained conditions:

val score = 78

if (score >= 90) {
    println("Grade A — Excellent")
} else if (score >= 80) {
    println("Grade B — Good")
} else if (score >= 70) {
    println("Grade C — Fair")
} else if (score >= 60) {
    println("Grade D — Poor")
} else {
    println("Grade E — Failed")
}

if as an Expression #

In Kotlin, if returns the value of the block that executes. The last value in a block is the one returned. This eliminates the need for a ternary operator (condition ? a : b) found in Java, JavaScript, and other languages.

val number = 42

// if as an expression — the result is stored in a variable
val description = if (number > 0) "positive" else "zero or negative"
println(description)  // positive

// Multiline version — the last value of each block is returned
val category = if (number > 100) {
    println("Computing large category...")
    "large"
} else if (number > 10) {
    println("Computing medium category...")
    "medium"
} else {
    "small"
}
println(category)  // medium

if Expressions Replace the Ternary #

Because Kotlin doesn’t have a ternary operator, the if expression is its replacement:

val age = 20
val status = "You are " + if (age >= 18) "an adult" else "a minor"

// In a string template
println("Status: ${if (age >= 18) "adult" else "minor"}")

// As a function argument
display(if (active) "Active" else "Inactive")

Requirements for if as an Expression #

When if is used as an expression (its value is used), else is mandatory. Without else, the compiler doesn’t know what value to return if the condition isn’t met.

// ANTI-PATTERN: if expression without else — won't compile
val result = if (x > 0) "positive"  // ✗ error: 'if' must have both main and else branches

// CORRECT: always have else when used as an expression
val result = if (x > 0) "positive" else "not positive"

Guard Clauses — Early Return with if #

One of the most important patterns in clean coding is the guard clause: check invalid conditions at the start of a function and return early, instead of wrapping all logic in nested if conditions.

// ANTI-PATTERN: logic wrapped in nested ifs (pyramid of doom)
fun processTransaction(user: User?, amount: Double) {
    if (user != null) {
        if (user.active) {
            if (amount > 0) {
                if (user.balance >= amount) {
                    // only here does the main logic start...
                    user.balance -= amount
                    println("Transaction successful")
                } else {
                    println("Insufficient balance")
                }
            } else {
                println("Amount must be positive")
            }
        } else {
            println("Account inactive")
        }
    } else {
        println("User not found")
    }
}

// CORRECT: guard clause — every invalid condition is handled early
fun processTransaction(user: User?, amount: Double) {
    if (user == null) {
        println("User not found")
        return
    }
    if (!user.active) {
        println("Account inactive")
        return
    }
    if (amount <= 0) {
        println("Amount must be positive")
        return
    }
    if (user.balance < amount) {
        println("Insufficient balance")
        return
    }

    // Main logic — only reached when all conditions are met
    user.balance -= amount
    println("Transaction successful")
}

Guard clauses keep the main logic flat on the left, not buried under deep indentation. Each early return is an explicit statement of why the function can’t continue.


when — A Much More Powerful Switch #

when in Kotlin goes far beyond switch in Java. It can match a single value, ranges, types, arbitrary conditions, or combinations — and like if, it’s also an expression.

Basic when #

val dayNumber = 3

val dayName = when (dayNumber) {
    1 -> "Monday"
    2 -> "Tuesday"
    3 -> "Wednesday"
    4 -> "Thursday"
    5 -> "Friday"
    6 -> "Saturday"
    7 -> "Sunday"
    else -> "Invalid day"
}

println(dayName)  // Wednesday

Multiple Values in One Branch #

Several values can be handled in a single branch by separating them with commas:

val monthNumber = 4

val season = when (monthNumber) {
    12, 1, 2  -> "Rainy Season (Peak)"
    3, 4, 5   -> "Transition to Dry Season"
    6, 7, 8   -> "Dry Season"
    9, 10, 11 -> "Transition to Rainy Season"
    else      -> "Invalid month"
}

println(season)  // Transition to Dry Season

when with Ranges #

val score = 85

val grade = when (score) {
    in 90..100 -> "A"
    in 80..89  -> "B"
    in 70..79  -> "C"
    in 60..69  -> "D"
    in 0..59   -> "E"
    else       -> "Invalid score"
}

println(grade)  // B

when as a Statement vs Expression #

Like if, when can be used as a statement or an expression:

val code = 404

// As a statement (no else needed)
when (code) {
    200 -> println("OK")
    404 -> println("Not found")
    500 -> println("Server error")
}

// As an expression (else is required if not exhaustive)
val message = when (code) {
    200 -> "Success"
    201 -> "Created"
    400 -> "Invalid request"
    401 -> "Unauthorized"
    403 -> "Forbidden"
    404 -> "Not found"
    in 500..599 -> "Server error"
    else -> "Unknown status: $code"
}

Exhaustive when — Extra Safety from the Compiler #

When when is used as an expression with enums or sealed classes, Kotlin can check whether all possibilities have been handled. This is called the exhaustive check — and it’s an extremely valuable feature for preventing bugs.

enum class OrderStatus {
    PENDING, PROCESSING, SHIPPED, COMPLETED, CANCELLED
}

fun statusLabel(status: OrderStatus): String {
    return when (status) {
        OrderStatus.PENDING    -> "Awaiting confirmation"
        OrderStatus.PROCESSING -> "Being processed"
        OrderStatus.SHIPPED    -> "In transit"
        OrderStatus.COMPLETED  -> "Order completed"
        OrderStatus.CANCELLED  -> "Order cancelled"
        // No else needed — the compiler knows all cases are handled
    }
}

The benefit of the exhaustive check is felt when the enum is expanded. If you add a new entry to OrderStatus, the compiler immediately gives an error in every when that doesn’t handle the new entry — no case silently slips through.

// After adding OrderStatus.RETURNED to the enum:
// fun statusLabel() will produce a compilation error — forcing you to handle the new case

Exhaustive when with Sealed Classes #

Sealed classes provide the same exhaustive check benefit, but for more complex type hierarchies:

sealed class OperationResult {
    data class Success(val data: String) : OperationResult()
    data class Failure(val message: String, val code: Int) : OperationResult()
    object Loading : OperationResult()
}

fun handleResult(result: OperationResult) {
    when (result) {
        is OperationResult.Success -> println("Data: ${result.data}")
        is OperationResult.Failure -> println("Error ${result.code}: ${result.message}")
        is OperationResult.Loading -> println("Loading...")
        // No else needed — all subtypes are covered
    }
}

handleResult(OperationResult.Success("response from server"))
handleResult(OperationResult.Failure("Connection lost", 503))
handleResult(OperationResult.Loading)

when with Type Checks #

when can be used to match types and automatically perform smart casts inside the matching branch:

fun processValue(value: Any): String {
    return when (value) {
        is String  -> "Text '${value.uppercase()}' (${value.length} characters)"
        is Int     -> "Integer: ${value * 2}"
        is Double  -> "Decimal: ${\"%.2f\".format(value)}"
        is Boolean -> if (value) "True" else "False"
        is List<*> -> "List containing ${value.size} elements"
        is Map<*, *> -> "Map containing ${value.size} entries"
        null       -> "Empty value (null)"
        else       -> "Unknown type: ${value::class.simpleName}"
    }
}

println(processValue("kotlin"))          // Text 'KOTLIN' (6 characters)
println(processValue(42))                // Integer: 84
println(processValue(3.14))              // Decimal: 3.14
println(processValue(listOf(1, 2, 3)))   // List containing 3 elements
println(processValue(null))              // Empty value (null)

Inside each is branch, Kotlin automatically performs a smart cast — no explicit cast needed. In the is String branch, value can already be treated as String.


when Without Arguments #

when without arguments serves as a replacement for long if-else chains. Each branch can be any Boolean expression.

val temperature = 35.0
val humidity = 80

val weatherCondition = when {
    temperature >= 38                -> "Very hot and dangerous"
    temperature >= 35 && humidity >= 80 -> "Hot and humid — feels stuffier"
    temperature >= 35                -> "Hot"
    temperature >= 25                -> "Warm"
    temperature >= 15                -> "Cool"
    temperature >= 0                 -> "Cold"
    else                             -> "Very cold — below freezing"
}

println(weatherCondition)  // Hot and humid — feels stuffier

This is much cleaner than a series of if-else if when the conditions are complex and don’t all depend on the same single variable.


when with Multi-Statement Blocks #

Each when branch can contain a multiline code block. The last value in the block is the one returned (if when is used as an expression):

val action = "PAY"
val amount = 150_000.0

val result = when (action) {
    "PAY" -> {
        println("Processing payment of Rp${amount.toLong()}...")
        val adminFee = amount * 0.01
        val total = amount + adminFee
        "Payment successful. Total: Rp${total.toLong()}"  // the returned value
    }
    "REFUND" -> {
        println("Processing refund...")
        "Refund will be processed within 3-5 business days"
    }
    "CHECK_BALANCE" -> {
        println("Fetching balance data...")
        "Current balance: Rp500.000"
    }
    else -> "Unknown action: $action"
}

println(result)

when with Additional Conditions — Guards #

A when branch can be extended with an additional condition using if after the pattern:

data class User(val name: String, val age: Int, val premium: Boolean)

fun categorizeUser(user: User): String {
    return when {
        user.premium && user.age >= 18 -> "Premium Adult"
        user.premium && user.age < 18  -> "Premium Junior"
        !user.premium && user.age >= 18 -> "Regular Adult"
        else -> "Regular Junior"
    }
}

Control Flow in Kotlin #

Understanding the relationship between if and when helps you choose the right one for each situation:

flowchart TD
    A{Conditions\nneed checking?} --> B{One variable\nwith many values?}
    B -- Yes --> C["when(variable) { ... }"]
    B -- No --> D{Varied\ncomplex conditions?}
    D -- Yes --> E["when { condition -> ... }"]
    D -- No --> F{Need the\ncondition's value?}
    F -- Yes --> G["val x = if (condition) a else b"]
    F -- No --> H["if (condition) { ... } else { ... }"]
    C --> I{All cases\nmust be handled?}
    I -- Yes --> J["Use enum/sealed class\nfor exhaustive check"]
    I -- No --> K["Add else"]

Choosing Between if and when #

USE if when:
  ✓ One or two simple conditions
  ✓ Conditions involve range comparisons or free Boolean expressions
  ✓ Guard clauses (early returns) at the start of a function
  ✓ Ternary replacement: val x = if (a) b else c

USE when when:
  ✓ Matching one variable against many possible values
  ✓ Handling various types with smart casts
  ✓ Replacing long if-else if chains (more than 3 conditions)
  ✓ Working with enums or sealed classes for exhaustive checks
  ✓ Code reads better with when than with nested if-else

Real Example: Processing HTTP Responses #

data class HttpResponse(val code: Int, val body: String?)

fun handleResponse(response: HttpResponse): String {
    // Guard clause: basic validation first
    if (response.code < 100 || response.code > 599) {
        return "Invalid response code: ${response.code}"
    }

    // when for handling based on the code
    return when (response.code) {
        200 -> "Success: ${response.body ?: "No content"}"
        201 -> "New data created"
        204 -> "Success without content"
        301, 302 -> "Redirected to another URL"
        400 -> "Invalid request — check the parameters"
        401 -> "Authentication required — please log in"
        403 -> "Access denied"
        404 -> "Resource not found"
        429 -> "Too many requests — try again later"
        in 500..599 -> {
            val errorMessage = response.body ?: "No details"
            "Server error (${response.code}): $errorMessage"
        }
        else -> "Unknown response code: ${response.code}"
    }
}

println(handleResponse(HttpResponse(200, "User data")))
println(handleResponse(HttpResponse(404, null)))
println(handleResponse(HttpResponse(503, "Service Unavailable")))

Output:

Success: User data
Resource not found
Server error (503): Service Unavailable

Summary #

  • if is an expression — in Kotlin, if returns a value so it can be used directly on the right side of an assignment, as a function argument, or in a string template. This eliminates the need for a ternary operator.
  • else is required when if is used as an expression — the compiler needs a value for every possible condition. When used as a statement (its result isn’t used), else is optional.
  • Guard clauses are better than nested ifs — check invalid conditions at the start of a function with early returns, instead of wrapping the main logic in a deep if pyramid.
  • when is a much more powerful switch — it can match single values, multiple values, ranges, types, and free Boolean conditions. Each branch can be a single value, a multiline block, or a combination.
  • when without arguments replaces if-else chains — if more than two or three varied conditions need checking, when { } is cleaner than if-else if-else if.
  • Exhaustive checks with enums and sealed classes — when when is used as an expression with enums or sealed classes, the compiler verifies all cases are handled. Adding a new entry is automatically detected as a compilation error.
  • Smart casts in when — after an is TypeX branch, the variable is automatically treated as TypeX without an explicit cast.
  • when as an expression needs else — unless all possibilities are already exhaustive (enum/sealed class). When used as a statement, else is optional.

← Previous: Operators   Next: Loops →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact