---
title: 'Control Flow'
source: 'https://academia.sh/en/courses/kotlin/control-flow'
course: 'The Kotlin Language'
language: en
updated: '2026-10-21T05:02:39+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Shows `if` and `when` used as expressions that produce a value'
    - 'Writes type checks with `is` and ranges with `..`, `until`, `downTo`, and `step`'
    - 'Explains that a `when` branch matches the value with `==` and what result this gives for an ordinary class'
---

# Control Flow

Introduces the `if` and `when` expressions, the `is` check, and the `for` loop; measures that a `when` branch matches with `==` and does not hit for an ordinary class instance.

In string templates we saw that the same text can be built in two ways. In this lesson we move on to flow: choosing a value according to a condition, distributing a value to branches, walking over a range. At the end we find the identity criterion again: `when` uses `==` while matching a value against a branch, and an instance with no equality cannot hit the branch.

## if, when, and for

In Kotlin, `if` is an expression: it produces a value, and that value can be assigned to a variable. A separate ternary operator is not needed; you write `val label = if (score >= 50) "passed" else "failed"`. `when` is also an expression. In the form without a subject, every branch carries a logical expression and the first true branch runs. In the form tied to a value, you write `when (value)`; each branch begins either with a value or with a type check using `is`. When `when` is used as an expression, the compiler wants to be sure that all cases are covered: in the form without a subject an `else` branch is needed; in the form with a subject, if `Boolean`, an enumeration, or a sealed class is listed with all of its cases, `else` is not required.

The `for` loop walks over a range or a collection. The range `1..3` is a closed range: the upper bound is included. `1 until 7` leaves the upper bound out. `step` sets the step, and `3 downTo 1` counts backward.

In this lesson too, the measurements are made with the helper file introduced in the first lesson. The `Criterion` class takes a value, its twin, and a different value, and asks three questions; the file is given below exactly as it was.

```kotlin
// Criterion.kt — classifies the criterion that makes two values equal
class Criterion(val label: String, first: Any?, twin: Any?, other: Any?) {
    val sameObject = first === twin
    val equalTwin = first == twin
    val equalOther = first == other

    val name: String = when {
        sameObject -> "singleton"
        !equalTwin -> "identity"
        !equalOther -> "content"
        else -> "partial-content"
    }

    fun report() {
        println("$label: same-object=$sameObject, twin=$equalTwin, other=$equalOther -> $name")
    }
}

fun summarize(checks: List<Criterion>) {
    for (name in listOf("singleton", "identity", "content", "partial-content")) {
        println("$name: ${checks.count { it.name == name }}")
    }
}
```

The program below runs these together. `grade` turns a score into a word with a `when` that has no subject. `describe` separates an `Any` value by its type with `is`: for an `Int` it states the double, and for a `String` it states the length.

```kotlin
// Main.kt — if, when, is, and for
fun grade(score: Int): String = when {
    score >= 90 -> "excellent"
    score >= 70 -> "good"
    else -> "pass"
}

fun describe(value: Any): String = when (value) {
    is Int -> "integer, double is ${value * 2}"
    is String -> "string, length ${value.length}"
    else -> "unknown"
}

fun main() {
    val score = 82
    val label = if (score >= 50) "passed" else "failed"
    println("$label, ${grade(score)}")
    println(describe(21))
    println(describe("kotlin"))
    println((1..3).joinToString(" "))
    println((1 until 7 step 2).joinToString(" "))
    println((3 downTo 1).joinToString(" "))
}
```

```
passed, good
integer, double is 42
string, length 6
1 2 3
1 3 5
3 2 1
```

In the first line, the `label` variable took the value `passed` through the `if` expression, and the call `grade(82)` returned `good`, because 82 is below ninety and above seventy. The second and third lines show the two branches of the `describe` function: because `21` is an `Int`, its double, 42, was printed; because `"kotlin"` is a `String`, its length, 6, was printed. Inside these branches, the `value` variable was reached with the new form of its type; this feature is called a smart cast. The last three lines are ranges: `1..3` gives all three, `1 until 7 step 2` gives the values one, three, five, and `3 downTo 1` counts backward.

**LB31.** The `when` expression was measured with integer and string branches; the exhaustive `when` over an enumeration and over `Boolean` is measured below, and the check over a sealed class is outside this lesson.

**LB32.** Ranges were printed only with `joinToString`; what an empty range does in a loop is measured separately below.

## A when Branch Matches with ==

The measurement of the lesson looks at the matching rule of the `when` branch. When a branch expression is written inside `when (value)`, Kotlin makes the comparison `value == branch`. For this reason, whether a branch hits depends on the equality criterion. The `branchMatches` function asks this directly: it matches the `value` it receives against `branch` through `when` and returns `true` or `false`.

Three cases are tried. The first is `Point(1, 2)` against `Point(1, 2)`: a data class, that is, the content criterion. The second is `Box(1)` against `Box(1)`: an ordinary class, that is, identity. The third is the same `box` instance matched against itself. Then the `is Box` branch is given to the same `Box(1)` value. Finally, `Criterion` classifies three observations.

```kotlin
// Main.kt — a when branch matches with ==
class Box(val size: Int)

data class Point(val x: Int, val y: Int)

fun branchMatches(value: Any, branch: Any): Boolean = when (value) {
    branch -> true
    else -> false
}

fun main() {
    val box = Box(1)
    println(branchMatches(Point(1, 2), Point(1, 2)))
    println(branchMatches(Box(1), Box(1)))
    println(branchMatches(box, box))
    println(when (Box(1)) {
        is Box -> "is Box branch hits"
        else -> "does not hit"
    })
    val checks = listOf(
        Criterion("when-branch-box", Box(1), Box(1), Box(2)),
        Criterion("when-branch-point", Point(1, 2), Point(1, 2), Point(1, 3)),
        Criterion("range", 1..3, 1..3, 1..4),
    )
    for (check in checks) {
        check.report()
    }
    summarize(checks)
}
```

```
true
false
true
is Box branch hits
when-branch-box: same-object=false, twin=false, other=false -> identity
when-branch-point: same-object=false, twin=true, other=false -> content
range: same-object=false, twin=true, other=false -> content
singleton: 0
identity: 1
content: 2
partial-content: 0
```

`branchMatches(Point(1, 2), Point(1, 2))` gave `true`: in a data class `==` looks at the content, and the branch hit. `branchMatches(Box(1), Box(1))` gave `false`: even though the two `Box(1)` instances carry the same constructor argument, they are separate objects, and in an ordinary class `==` looks at identity. The same `box` instance matched itself, because an object is equal to itself. The `is Box` branch looks at the type, not the value: even a freshly built `Box(1)` hits the `is Box` branch. For this reason the line `is Box branch hits` was printed.

The three classification lines summarize this. `when-branch-box` is identity: `==` with the twin gives `false`. `when-branch-point` is content: `==` with the twin gives `true`, and with the different value gives `false`. `range` is also content: the range `1..3` is equal to another separately built `1..3` range and is not equal to the `1..4` range. The summary lines show zero singleton, one identity, two content, and zero partial content.

**LB33.** The `branchMatches` function takes the branch as a variable; that the same `==` rule holds when a constant branch is written inside `when` was not measured separately in this lesson.

**LB34.** Range equality was measured with `IntRange`; the same behavior is not claimed for `LongRange` and `CharRange`.

## when with a Range, a Multi-Value Branch, and an Enumeration

`when` branches are not limited to a single value. The block below asks three things: in which order a comma-separated multi-value branch, a range branch with `in`, and a branch that catches the outside of the range are evaluated when used together; whether a `when` over an enumeration (enum) compiles without `else`; and whether a `when` over `Boolean` closes with two values.

```kotlin
// Main.kt — when: multi-value branch, range, and enumeration
enum class Level { LOW, MEDIUM, HIGH }

fun classify(n: Int): String = when (n) {
    0 -> "zero"
    1, 2, 3 -> "few"
    in 4..9 -> "medium"
    !in 0..99 -> "out of range"
    else -> "many"
}

fun weight(level: Level): Int = when (level) {
    Level.LOW -> 1
    Level.MEDIUM -> 5
    Level.HIGH -> 10
}

fun main() {
    println(listOf(0, 2, 7, 50, 120).map { classify(it) })
    println(Level.entries.map { weight(it) })
    val flag = true
    println(when (flag) {
        true -> "yes"
        false -> "no"
    })
}
```

```
[zero, few, medium, many, out of range]
[1, 5, 10]
yes
```

For the inputs 0, 2, 7, 50, and 120 the result was `zero`, `few`, `medium`, `many`, `out of range`. The value 2 fell on the branch `1, 2, 3`, and 7 on the branch `in 4..9`. The value 50 did not hit the out-of-range branch and went to the `else` branch, while 120 hit the out-of-range branch. The rule is this: branches are tested from top to bottom and the first one that hits runs. Because the out-of-range branch was written before the `else` branch, the value 120 reached it; had the `else` branch been written earlier, the `out of range` branch could never have been reached; this reversed order could not be measured because it does not compile.

The second line gave `[1, 5, 10]`. The `weight` function carries no `else` branch and still compiles: the compiler sees that the branches cover all three values of the `Level` enumeration. The frequent mistake is to add a new value to the enumeration and forget to update the `when` expressions; an expression without an `else` branch gives a compile error in this case and finds the missing branch. If `else` is written, this guarantee is lost. This holds because `when` is used as an expression.

The third line gave `yes`: because `Boolean` is a type with two values, the `true` and `false` branches are exhaustive without `else`.

**LB35.** Exhaustiveness was measured with `enum class` and `Boolean`; the check of a `when` over a sealed class was not measured in this lesson.

## Empty Range, Step, and Labeled break

Ranges look easy and produce mistakes because of their edge cases. The block below counts an empty range, builds a `downTo` and a `..` that use `step`, tries a zero step, and leaves a nested loop with a labeled `break`.

```kotlin
// Main.kt — empty range, step, and labeled break
fun main() {
    println((5..1).count())
    println((1..0).toList())
    println((10 downTo 1 step 3).toList())
    println((1..10 step 4).last)
    try {
        println((1..3 step 0).toList())
    } catch (e: IllegalArgumentException) {
        println(e.message)
    }
    var visited = 0
    outer@ for (row in 1..3) {
        for (col in 1..3) {
            if (row * col == 4) break@outer
            visited++
        }
    }
    println(visited)
}
```

```
0
[]
[10, 7, 4, 1]
9
Step must be positive, was: 0.
4
```

`(5..1).count()` gave `0` and `(1..0).toList()` gave an empty list: the `..` operator works from smaller to larger, and if the start is larger than the end the range is empty and the loop never runs. If you need to count backward, `downTo` must be written. This is the frequent mistake: a person who writes `for (i in 5..1)` expects a countdown, and the program silently does nothing.

The list `10 downTo 1 step 3` became `[10, 7, 4, 1]`. The `last` value of the range `1..10 step 4` came out as `9`, that is, not 10: if the step does not fit the upper bound exactly, the last value stays below the bound. A zero step threw an `IllegalArgumentException` with the message `Step must be positive, was: 0.`; the step has to be positive, and counting backward is done with `downTo`, not with a negative step.

The last line gave `4`. In the inner loop, when `row * col == 4`, `break@outer` ended both the inner and the outer loop. A `break` without a label would have ended only the innermost loop. The visit count is four in total: three in row 1, one in row 2, since the break happens at the second column of row 2. In Java, a labeled `break` works the same way; the syntax is `outer:` with `break outer;`.

**LB36.** The range measurements were made with `Int`; step and overflow behavior in a `Long` range was not measured in this lesson.

## try Is an Expression

In Kotlin, `try` also produces a value. The block below makes the value of a `try` expression the return of a function, looks at when the `finally` block runs, and uses a function call inside an `if` expression. Finally, `firstOrNull` and the elvis operator are tried together.

```kotlin
// Main.kt — try is an expression
fun parse(text: String): Int = try {
    text.toInt()
} catch (e: NumberFormatException) {
    -1
} finally {
    println("tried: $text")
}

fun main() {
    println(parse("42"))
    println(parse("kotlin"))
    val label = if (parse("7") > 5) "big" else "small"
    println(label)
    val firstNegative = listOf(3, -2, 5).firstOrNull { it < 0 } ?: 0
    println(firstNegative)
}
```

```
tried: 42
42
tried: kotlin
-1
tried: 7
big
-2
```

The first call, `parse("42")`, printed first the line `tried: 42`, which is the output of the `finally` block, and then the return value `42`: `finally` runs before the value reaches the caller. In the second call, because `"kotlin"` could not be converted to a number, the `catch` branch ran and returned `-1`; again `tried: kotlin` was printed first. In the third line, the call `parse("7")` was used inside `if`: `tried: 7` was printed, and because 7 is greater than 5, `big` was written.

The frequent mistake is to put an expression that returns a value in the `finally` block and to assume that it will change the value of the try expression. Here, even though `println` is the last expression, the value of the `finally` block does not join the value of the expression; the returned value is the last expression of the `try` or `catch` block. The last line gave `-2`: the first element in the list that satisfied the condition `it < 0` was `-2`; if there had been none, the elvis operator would have returned `0`, but this branch was not measured. In Java, `try` is a statement and does not produce a value.

**LB37.** The `try` measurements were made with `NumberFormatException`; how the value is overridden when a `return` is written in the `finally` block was not measured in this lesson.

## Habits That Come from the Measurements

The common lesson of the measurements in flow control is that edge cases are silent. An empty range does not give an error; it only does nothing. If the step value does not fit the bound exactly, as in `1..10 step 4`, the last value stays below the bound. A zero step, on the other hand, raises an exception with a single message. Of these three cases, only the last appears as an error; the others produce wrong output.

The absence of an `else` branch in a `when` expression is a design tool. If you do not write `else` on an enumeration, the compiler directs you to all the `when` expressions when a new value is added. If you write `else`, you have turned off this warning yourself. For this reason the `else` branch should be used where it really means "everything else", for example in integer ranges; it should not be used on a closed set of values. Order also needs attention: because branches are tested from top to bottom, the specific branch is written before the general one, and in the measurement the value 120 reached the out-of-range branch because it was before `else`.

In a `try` expression, the value of the `finally` block does not join the result, but its side effect always runs. Work such as closing a resource and writing a log is done in `finally` for this reason, while the calculation of the value is done in `try` and `catch`. In Java, `try` is a statement; to assign the value to a variable, one has to define a variable outside. In Kotlin, writing `val result = try { ... } catch ...` makes this variable a read-only `val` and gives shorter code.

A labeled `break` is the most readable way to leave nested loops, but its use should be kept limited. In most cases, moving the search into a separate function and leaving with `return` requires no label and is also easy to test.

## The Bounding Measurement

The identity criterion holds only for branches matched with `==`. The `is Box` branch hits, because a type check is not a question of equality. That is, two branches can stand side by side in the same `when`: one does not hit because of identity, and the other hits because of the type. A program that builds a second `Box(1)` instance and tries to match it inside `when` can never hit the branch unless `Box` is a data class; but when `is Box` is written, the branch hits.

The second limit is in ranges: `1..3` does not fall under the identity criterion. Two range objects are equal even if they were built separately. The equality of a range and the hitting of an `in` branch are different questions: the first was measured in this section, and the second in the `when` section.

## Summary

- `if` and `when` are expressions that produce a value; when `when` is used as an expression, all cases must be covered, which an `else` branch or the listing of all cases of an enumeration, `Boolean`, or sealed class subject provides.
- `is` is a type check and allows the value to be used inside the branch as a variable of that type.
- `..`, `until`, `downTo`, and `step` build ranges; `for` walks over these ranges.
- A branch inside `when (value)` matches with `value == branch`; because an ordinary class instance rests on identity, it cannot hit the branch.
- This lesson's criterion: **identity**; bounding measurement: the `is Box` branch hits and two `1..3` ranges fall under the content criterion.

## Next Step

We have seen the flow; now we move to the tool for packaging the flow: functions. We begin with parameters with default values, named arguments, and `vararg`. We measure whether taking the same function twice as a reference gives the same object or only equal content.
