Control Flow

This section documents Kotlin 2.4.x on the JVM, as published at kotlinlang.org, which is the reference these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against kotlinlang.org before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Kotlin’s control-flow constructs will look familiar, with one structural difference from Java that runs through the whole language: if and when are expressions, not only statements.

if as an Expression

if/else can produce a value directly, replacing Java’s ternary operator (which Kotlin does not have) and, for anything more than a one-liner, replacing an assignment split across several statements:

val a = 5
val b = 2

val max = if (a > b) a else b               // an expression -- no ternary operator needed

val description = if (max > 10) {
    "big"
} else {
    "small"                                  // the last expression in each branch is the branch's value
}

When used as an expression, if must have an else branch (the compiler needs a value on every path); used as a plain statement whose value is discarded, else remains optional exactly as in Java.

when as an Expression

when replaces Java’s switch and is far more general: its branches can match exact values, ranges, type checks, or arbitrary boolean conditions, and — like if — it can produce a value:

val x = 7

val category = when {                        // no subject: each branch is its own boolean condition
    x < 0 -> "negative"
    x == 0 -> "zero"
    x % 2 == 0 -> "positive even"
    else -> "positive odd"
}

fun describe(value: Any): String = when (value) {
    1, 2, 3 -> "small number"                 // comma-separated values share a branch
    in 4..10 -> "medium number"                // range membership
    is String -> "a string of length ${value.length}"  // type check + smart cast, see below
    else -> "something else"
}

When when has a subject (when (value)) over a sealed type or an enum, the compiler can enforce exhaustiveness at compile time with no else needed — covered in depth in Sealed Classes and Enums. Inside an is branch, value is automatically smart-cast to that type for the rest of the branch — no explicit cast needed (see Null Safety for the general smart-cast rule).

for and while

for iterates any Iterable (a collection, a range/progression, an array) — Kotlin has no C-style three-clause for. while/do-while test before/after each iteration exactly as in Java:

for (i in 1..5) print(i)                 // over a range -- see Operators and Ranges
for (c in "abc") print(c)                // Strings are Iterable<Char>
for ((index, value) in listOf("a", "b").withIndex()) {
    println("$index: $value")             // destructuring the IndexedValue -- see Destructuring
}

var n = 5
while (n > 0) {
    print(n--)                            // 54321
}

break, continue, and Labels

break/continue behave as in Java for the innermost loop; a label@ prefix lets either target an outer loop explicitly, which is also how a lambda’s implicit loop-like control flow (forEach, for example) can be exited early:

outer@ for (row in 0..2) {
    for (col in 0..2) {
        if (row + col == 3) break@outer      // leaves BOTH loops
        if (col == row) continue@outer        // next row
        println("$row,$col")
    }
}

listOf(1, 2, 3, 4).forEach lit@{
    if (it == 3) return@lit                   // "continue" for this one lambda invocation only
    println(it)
}

return@label inside a lambda is Kotlin’s way of returning from that lambda rather than the enclosing function — necessary because a bare return inside an inline lambda (like `forEach’s) returns from the enclosing function itself, which is almost never what’s wanted; see Lambdas and Higher-Order Functions for the full non-local-return rule.

See Also