Beginner

Control Flow in Kotlin

Master conditionals, when expressions, and loops in Kotlin with practical Docker-ready examples covering if/else, ranges, and loop control

Control flow is how a program decides what to do next: which branch to take, which block to repeat, and when to stop. Kotlin keeps the familiar if, for, and while from Java but reshapes them around one core idea—control structures are expressions, not just statements. An if can produce a value, a when can replace a sprawling switch, and ranges make loops read like plain English.

Because Kotlin is multi-paradigm (object-oriented and functional), it gives you both worlds: imperative loops when you want them, and expression-oriented branching that fits a functional style. There’s no ternary ?: operator in Kotlin—you don’t need one, because if already returns a value. Likewise, when is far more powerful than a C-style switch, supporting ranges, type checks, and arbitrary conditions.

In this tutorial you’ll learn how Kotlin handles conditionals with if/else, the versatile when expression, the full family of loops (for, while, do/while), range expressions, and loop control with break, continue, and labels. Every example is self-contained and runnable in Docker.

Conditionals: if/else as an Expression

In Kotlin, if/else works as both a statement and an expression. When used as an expression, the value of the chosen branch becomes the result—the last line of a block is its value.

Create a file named Conditionals.kt:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
fun main() {
    // if/else as a statement
    val temperature = 18
    if (temperature > 25) {
        println("It's warm outside")
    } else if (temperature > 15) {
        println("It's mild outside")
    } else {
        println("It's cold outside")
    }

    // if/else as an expression - Kotlin has no ternary operator because
    // if already returns a value
    val score = 82
    val grade = if (score >= 90) "A" else if (score >= 80) "B" else "C"
    println("Grade: $grade")

    // The expression form can use blocks; the last line is the value
    val a = 7
    val b = 12
    val max = if (a > b) {
        println("a is larger")
        a
    } else {
        println("b is larger")
        b
    }
    println("Max: $max")
}

When if is used as an expression (assigned to a variable), the else branch is required—the compiler must know every path produces a value.

The when Expression

when is Kotlin’s powerful replacement for the switch statement. It can match exact values, ranges, multiple values per branch, or—when used with no argument—act as a clean if/else-if chain. It also performs smart casts when checking types.

Create a file named When.kt:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
fun main() {
    // when as a switch/case replacement
    val day = 3
    val name = when (day) {
        1 -> "Monday"
        2 -> "Tuesday"
        3 -> "Wednesday"
        in 4..5 -> "Almost weekend"
        6, 7 -> "Weekend"
        else -> "Unknown"
    }
    println("Day $day is $name")

    // when without an argument acts like an if/else-if chain
    val n = 42
    val size = when {
        n < 0 -> "negative"
        n == 0 -> "zero"
        n < 100 -> "small"
        else -> "large"
    }
    println("$n is $size")

    // when can check types, with smart casts inside each branch
    val items = listOf(42, "hello", 3.14, true)
    for (item in items) {
        val description = when (item) {
            is Int -> "Int doubled is ${item * 2}"
            is String -> "String of length ${item.length}"
            is Double -> "Double rounded is ${item.toInt()}"
            else -> "Something else"
        }
        println(description)
    }
}

Notice the branches: in 4..5 matches a range, 6, 7 matches either value, and is Int checks a type while smart-casting item so you can call item * 2 directly.

Loops: for, while, and do/while

Kotlin’s for loop iterates over anything that provides an iterator—most commonly ranges and collections. Ranges (1..5, 10 downTo 0, 0 until 4) make intent clear, and withIndex() gives you both position and value.

Create a file named Loops.kt:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
fun main() {
    // for over an inclusive range
    print("Countup: ")
    for (i in 1..5) {
        print("$i ")
    }
    println()

    // downTo counts backwards, step changes the increment
    print("Countdown: ")
    for (i in 10 downTo 0 step 2) {
        print("$i ")
    }
    println()

    // until creates a half-open range (excludes the upper bound)
    print("Indices: ")
    for (i in 0 until 4) {
        print("$i ")
    }
    println()

    // iterating a collection with both index and value
    val fruits = listOf("apple", "banana", "cherry")
    for ((index, fruit) in fruits.withIndex()) {
        println("$index: $fruit")
    }

    // while loop runs while the condition is true
    var count = 3
    while (count > 0) {
        println("T-minus $count")
        count--
    }
    println("Liftoff!")

    // do/while runs the body at least once before checking
    var attempts = 0
    do {
        attempts++
        println("Attempt $attempts")
    } while (attempts < 2)
}

Ranges are first-class objects: 1..5 is an IntRange, and downTo, until, and step are infix functions that build new ranges. This is why loops read so naturally.

Loop Control: break, continue, and Labels

continue skips to the next iteration and break exits the loop entirely. For nested loops, Kotlin lets you tag a loop with a label (outer@) and break or continue that specific loop with break@outer.

Create a file named LoopControl.kt:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
fun main() {
    // continue skips the rest of the current iteration
    print("Odd numbers: ")
    for (i in 1..10) {
        if (i % 2 == 0) continue
        print("$i ")
    }
    println()

    // break exits the loop early
    print("Until 5: ")
    for (i in 1..10) {
        if (i > 5) break
        print("$i ")
    }
    println()

    // a labeled break escapes the outer loop from inside a nested loop
    print("Pairs: ")
    outer@ for (i in 1..3) {
        for (j in 1..3) {
            if (i == j) continue
            if (i + j > 4) break@outer
            print("($i,$j) ")
        }
    }
    println()
}

Without the label, break would only exit the inner loop. The outer@ label lets break@outer jump out of both loops at once—handy when a nested search finds what it needs.

Running with Docker

The Zenika Kotlin image compiles each example to a runnable JAR and executes it. Compile any of the four files by swapping the .kt filename.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# Pull the official image
docker pull zenika/kotlin:1.4

# Run the conditionals example
docker run --rm -v $(pwd):/app -w /app zenika/kotlin:1.4 \
  sh -c "kotlinc Conditionals.kt -include-runtime -d app.jar && java -jar app.jar"

# Run the when example
docker run --rm -v $(pwd):/app -w /app zenika/kotlin:1.4 \
  sh -c "kotlinc When.kt -include-runtime -d app.jar && java -jar app.jar"

# Run the loops example
docker run --rm -v $(pwd):/app -w /app zenika/kotlin:1.4 \
  sh -c "kotlinc Loops.kt -include-runtime -d app.jar && java -jar app.jar"

# Run the loop control example
docker run --rm -v $(pwd):/app -w /app zenika/kotlin:1.4 \
  sh -c "kotlinc LoopControl.kt -include-runtime -d app.jar && java -jar app.jar"

Expected Output

Running Conditionals.kt:

It's mild outside
Grade: B
b is larger
Max: 12

Running When.kt:

Day 3 is Wednesday
42 is small
Int doubled is 84
String of length 5
Double rounded is 3
Something else

Running Loops.kt:

Countup: 1 2 3 4 5 
Countdown: 10 8 6 4 2 0 
Indices: 0 1 2 3 
0: apple
1: banana
2: cherry
T-minus 3
T-minus 2
T-minus 1
Liftoff!
Attempt 1
Attempt 2

Running LoopControl.kt:

Odd numbers: 1 3 5 7 9 
Until 5: 1 2 3 4 5 
Pairs: (1,2) (1,3) (2,1) 

Key Concepts

  • if is an expression — It returns a value, so Kotlin has no ternary operator. The last expression in a branch block is its result.
  • when replaces switch — It matches values, ranges (in 4..5), multiple options (6, 7), and types (is Int) with automatic smart casts.
  • Argument-less when — Omitting the subject turns when into a clean if/else-if chain where each branch holds a boolean condition.
  • Ranges power loops1..5 (inclusive), 0 until 4 (half-open), 10 downTo 0 (descending), and step 2 compose to express iteration clearly.
  • withIndex() — Destructure (index, value) directly in a for loop instead of managing a counter manually.
  • do/while — Runs its body at least once before testing the condition, unlike a plain while.
  • Labels for nested loops — Tag a loop with label@ and use break@label or continue@label to control the outer loop from inside a nested one.
  • Exhaustiveness — When if or when is used as an expression, an else branch is required so every path yields a value.

Running Today

All examples can be run using Docker:

docker pull zenika/kotlin:1.4
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining