In the previous tutorial, you learned about functions. Now let’s learn about control flow — how to make decisions and repeat actions in your code.

Kotlin’s control flow is similar to Java, but with important improvements. if and when are expressions that return values. when replaces switch and is much more powerful.

In this tutorial, you will learn:

  • if as an expression
  • when — Kotlin’s powerful pattern matching
  • Ranges
  • for loops
  • while and do-while loops
  • break, continue, and labels

if Expression

In Java, if is a statement. In Kotlin, if is an expression — it returns a value.

// if as an expression — returns a value
val max = if (a > b) a else b

This means you don’t need the ternary operator (? :) from Java. Kotlin’s if does the same thing.

Basic if-else

fun maxOf(a: Int, b: Int): Int {
    return if (a > b) a else b
}

println(maxOf(10, 20)) // 20

As a single-expression function:

fun minOf(a: Int, b: Int) = if (a < b) a else b

if-else if-else Chain

fun classifyTemperature(temp: Int): String {
    return if (temp < 0) {
        "Freezing"
    } else if (temp < 15) {
        "Cold"
    } else if (temp < 25) {
        "Nice"
    } else if (temp < 35) {
        "Hot"
    } else {
        "Very hot"
    }
}

println(classifyTemperature(22))  // Nice
println(classifyTemperature(-5))  // Freezing
println(classifyTemperature(40))  // Very hot

When if is used as an expression (returning a value), the else branch is required. This makes sure the expression always has a value.

when Expression

when is Kotlin’s replacement for Java’s switch. It is much more powerful.

Basic when

fun dayType(day: String): String {
    return when (day) {
        "Monday", "Tuesday", "Wednesday", "Thursday", "Friday" -> "Weekday"
        "Saturday", "Sunday" -> "Weekend"
        else -> "Unknown"
    }
}

println(dayType("Monday"))   // Weekday
println(dayType("Saturday")) // Weekend

Each branch uses -> to separate the condition from the result. Multiple values can share a branch with commas.

when with Ranges

fun gradeDescription(score: Int): String {
    return when (score) {
        in 90..100 -> "Excellent"
        in 80..89 -> "Good"
        in 70..79 -> "Average"
        in 60..69 -> "Below average"
        in 0..59 -> "Fail"
        else -> "Invalid score"
    }
}

println(gradeDescription(85)) // Good
println(gradeDescription(55)) // Fail

when with Type Checks

fun describe(value: Any): String {
    return when (value) {
        is String -> "String with length ${value.length}"
        is Int -> "Integer: $value"
        is Double -> "Double: $value"
        is Boolean -> if (value) "True" else "False"
        is List<*> -> "List with ${value.size} items"
        else -> "Unknown type"
    }
}

println(describe(42))      // Integer: 42
println(describe("Hello")) // String with length 5
println(describe(true))    // True

After is String, Kotlin smart casts value to String, so you can use .length directly.

when Without an Argument

When used without an argument, when works like an if-else chain. Each branch is a Boolean condition:

fun classifyNumber(number: Int): String {
    return when {
        number < 0 -> "Negative"
        number == 0 -> "Zero"
        number % 2 == 0 -> "Positive even"
        else -> "Positive odd"
    }
}

println(classifyNumber(7))   // Positive odd
println(classifyNumber(-3))  // Negative
println(classifyNumber(0))   // Zero
println(classifyNumber(4))   // Positive even

Guard Conditions in when (Kotlin 2.2+)

Starting with Kotlin 2.2, you can add guard conditions to when branches using the if keyword. This lets you check additional conditions after the pattern match:

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

fun handleStatus(status: Status): String {
    return when (status) {
        is Status.Error if status.code == 404 -> "Not found: ${status.message}"
        is Status.Error if status.code >= 500 -> "Server error: ${status.message}"
        is Status.Error -> "Error ${status.code}: ${status.message}"
        is Status.Success -> "OK: ${status.data}"
        is Status.Loading -> "Loading..."
    }
}

Without guard conditions, you would need nested if statements or separate when expressions. Guard conditions keep the code flat and readable.

Another example with a simpler type:

fun classifyScore(score: Int): String {
    return when (score) {
        in 90..100 if score == 100 -> "Perfect score!"
        in 90..100 -> "Excellent"
        in 80..89 -> "Good"
        else -> "Keep studying"
    }
}

when as a Statement

You can also use when without returning a value:

fun printSeason(month: Int) {
    when (month) {
        12, 1, 2 -> println("Winter")
        3, 4, 5 -> println("Spring")
        6, 7, 8 -> println("Summer")
        9, 10, 11 -> println("Autumn")
        else -> println("Invalid month")
    }
}

Ranges

Ranges define a sequence of values. They are used in loops, when expressions, and if checks.

// Inclusive range — 1, 2, 3, 4, 5
val oneToFive = 1..5

// Exclusive end — 1, 2, 3, 4
val oneToFour = 1..<5

// Descending range — 5, 4, 3, 2, 1
val fiveToOne = 5 downTo 1

// With step — 0, 2, 4, 6, 8, 10
val evens = 0..10 step 2

// Descending with step — 10, 7, 4, 1
val countdown = 10 downTo 0 step 3

// Character range — a, b, c, d, e
val letters = 'a'..'e'

Check if a value is in a range:

val age = 25
println(age in 18..65)    // true
println(age !in 0..17)    // true

val grade = 'B'
println(grade in 'A'..'F') // true

for Loops

Iterate Over a Range

for (i in 1..5) {
    print("$i ") // 1 2 3 4 5
}

Iterate with Step

for (i in 0..10 step 2) {
    print("$i ") // 0 2 4 6 8 10
}

Iterate Backwards

for (i in 5 downTo 1) {
    print("$i ") // 5 4 3 2 1
}

Iterate Over a Collection

val fruits = listOf("Apple", "Banana", "Cherry")
for (fruit in fruits) {
    println("I like $fruit")
}

Iterate with Index

val fruits = listOf("Apple", "Banana", "Cherry")
for ((index, fruit) in fruits.withIndex()) {
    println("$index: $fruit")
}
// 0: Apple
// 1: Banana
// 2: Cherry

Iterate Over a String

for (char in "Kotlin") {
    print("$char ") // K o t l i n
}

Iterate Over a Map

val scores = mapOf("Alex" to 95, "Sam" to 87, "Jordan" to 92)
for ((name, score) in scores) {
    println("$name scored $score")
}

repeat

For simple repetition, use repeat:

repeat(3) { index ->
    println("Repeat $index")
}
// Repeat 0
// Repeat 1
// Repeat 2

while and do-while

while Loop

while runs as long as the condition is true:

var count = 0
var sum = 0
while (sum < 100) {
    sum += count
    count++
}
println("Sum reached $sum after $count iterations")

do-while Loop

do-while runs at least once, then checks the condition:

val numbers = mutableListOf<Int>()
var i = 1
do {
    numbers.add(i)
    i *= 2
} while (i <= 100)
println(numbers) // [1, 2, 4, 8, 16, 32, 64]

The difference: while might not run at all if the condition is false from the start. do-while always runs at least once.

break and continue

break — Exit the Loop

for (i in 1..100) {
    if (i * i > 50) {
        println("$i (because $i * $i = ${i * i})")
        break // Exit the loop
    }
}
// 8 (because 8 * 8 = 64)

continue — Skip to Next Iteration

for (i in 1..10) {
    if (i % 2 == 0) continue // Skip even numbers
    print("$i ") // 1 3 5 7 9
}

Labels

Labels let you break or continue from a specific loop when you have nested loops.

outer@ for (i in 1..3) {
    for (j in 1..3) {
        if (i == 2 && j == 2) {
            println("Breaking at i=$i, j=$j")
            break@outer // Breaks the OUTER loop
        }
        println("i=$i, j=$j")
    }
}

Output:

i=1, j=1
i=1, j=2
i=1, j=3
i=2, j=1
Breaking at i=2, j=2

Without @outer, break would only exit the inner loop. With break@outer, it exits both loops.

Practical Example: FizzBuzz

FizzBuzz is a classic programming exercise that uses all the control flow concepts:

fun fizzBuzz(n: Int): List<String> {
    val result = mutableListOf<String>()
    for (i in 1..n) {
        val value = when {
            i % 15 == 0 -> "FizzBuzz"
            i % 3 == 0 -> "Fizz"
            i % 5 == 0 -> "Buzz"
            else -> i.toString()
        }
        result.add(value)
    }
    return result
}

println(fizzBuzz(15))
// [1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, FizzBuzz]

Practical Example: Prime Numbers

fun isPrime(n: Int): Boolean {
    if (n < 2) return false
    for (i in 2..n / 2) {
        if (n % i == 0) return false
    }
    return true
}

fun nextPrime(after: Int): Int {
    var candidate = after + 1
    while (!isPrime(candidate)) {
        candidate++
    }
    return candidate
}

println(isPrime(7))      // true
println(isPrime(10))     // false
println(nextPrime(10))   // 11
println(nextPrime(20))   // 23

Summary

FeatureDescription
if/elseExpression that returns a value
when(value)Match on values, ranges, types
when {}Condition-based matching (no argument)
1..5Inclusive range
1..<5Exclusive end range
5 downTo 1Descending range
step 2Skip values in a range
for (i in range)Iterate over range/collection
while (cond)Loop while condition is true
do { } whileLoop at least once
breakExit loop
continueSkip to next iteration
break@labelExit labeled loop
is Type if condGuard condition in when (Kotlin 2.2+)

Source Code

You can find the complete source code for this tutorial on GitHub:

KT-6 Source Code on GitHub

What’s Next?

In the next tutorial, Kotlin Tutorial #7: Classes, Objects, and Data Classes, you will learn:

  • How to create classes in Kotlin
  • Constructors and properties
  • Inheritance and interfaces
  • Data classes
  • Objects and companion objects
  • Sealed classes

This is part 6 of the Kotlin Tutorial series. Check out Part 5: Functions if you missed it. Need a quick reference? See the Kotlin Cheat Sheet.