Loops #

Loops are the mechanism for executing a code block repeatedly — either a predetermined number of times, or while a certain condition holds. Kotlin provides three classic loop keywords: for, while, and do-while. But Kotlin also encourages a more declarative approach through higher-order functions like forEach, map, filter, and reduce — which are often more expressive and safer from common manual-loop errors like off-by-one bugs. This article covers all loop forms in Kotlin, when to use each one, and patterns that make looping code cleaner.

The for Loop #

for in Kotlin iterates over anything implementing Iterable — ranges, arrays, lists, maps, strings, and more. There’s no for (i = 0; i < n; i++) form like in Java or C — Kotlin replaces it with more declarative syntax.

Range Iteration #

// Inclusive at both ends: 1, 2, 3, 4, 5
for (i in 1..5) {
    print("$i ")
}
// 1 2 3 4 5

// Exclusive at the right end: 0, 1, 2, 3, 4
for (i in 0 until 5) {
    print("$i ")
}
// 0 1 2 3 4

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

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

// Backwards with a step: 10, 7, 4, 1
for (i in 10 downTo 1 step 3) {
    print("$i ")
}
// 10 7 4 1

Collection Iteration #

val languages = listOf("Kotlin", "Java", "Python", "Go")

// Element iteration
for (lang in languages) {
    println(lang)
}

// Iteration with index using withIndex()
for ((index, lang) in languages.withIndex()) {
    println("[$index] $lang")
}
// [0] Kotlin
// [1] Java
// [2] Python
// [3] Go

// Index-only iteration if elements aren't needed
for (i in languages.indices) {
    println("Position $i: ${languages[i]}")
}

Map Iteration #

val capitals = mapOf(
    "Indonesia" to "Jakarta",
    "Japan"     to "Tokyo",
    "France"    to "Paris",
    "Brazil"    to "Brasilia"
)

// Destructure map entries directly in for
for ((country, city) in capitals) {
    println("$country$city")
}

// If you only need keys or values
for (country in capitals.keys) print("$country ")
for (city in capitals.values) print("$city ")

String Iteration #

String can also be iterated character by character:

val word = "Kotlin"

for (character in word) {
    print("$character-")
}
// K-o-t-l-i-n-

// With index
for ((i, c) in word.withIndex()) {
    println("[$i] = '$c'")
}

The while Loop #

while evaluates the condition before each iteration. If the condition is false from the start, the code block is never executed at all.

var count = 1

while (count <= 5) {
    println("Iteration $count")
    count++
}

while is most appropriate when the number of iterations isn’t known in advance and depends on a dynamic condition:

// Connection attempt simulation
var attempts = 0
val maxAttempts = 3
var connected = false

while (!connected && attempts < maxAttempts) {
    attempts++
    println("Connection attempt $attempts...")
    connected = tryConnect()  // a function returning Boolean

    if (!connected && attempts < maxAttempts) {
        println("Failed, retrying in 2 seconds...")
        Thread.sleep(2_000)
    }
}

if (connected) {
    println("Connected successfully!")
} else {
    println("Connection failed after $maxAttempts attempts.")
}

Infinite Loop with while #

// Loop forever — there must be an exit mechanism (break or return)
while (true) {
    val input = readInput()
    if (input == "quit") break
    processInput(input)
}

The do-while Loop #

do-while evaluates the condition after each iteration. This guarantees the code block executes at least once, even if the condition is already false from the start.

var i = 10

do {
    println("Value of i: $i")
    i++
} while (i <= 5)
// Output: "Value of i: 10" — executes once even though the condition is immediately false

The most classic do-while use case is asking for user input until the input is valid:

var input: String
var number: Int

do {
    print("Enter a number between 1 and 10: ")
    input = readLine() ?: ""
    number = input.toIntOrNull() ?: -1

    if (number !in 1..10) {
        println("Invalid input. Try again.")
    }
} while (number !in 1..10)

println("You entered: $number")

This pattern is more natural than while because you don’t need to initialize the variable with a dummy value before the loop just to make the first condition evaluable.


break and continue #

break — Exit the Loop #

break stops the loop entirely and continues execution to the code after the loop:

val list = listOf(3, 7, 2, 9, 1, 5, 8, 4)

var target = 9
var position = -1

for ((index, value) in list.withIndex()) {
    if (value == target) {
        position = index
        break  // no need to continue after finding it
    }
}

if (position >= 0) {
    println("$target found at index $position")
} else {
    println("$target not found")
}

continue — Skip This Iteration #

continue skips the rest of the code in the current iteration and moves straight to the next one:

val numbers = listOf(1, -3, 5, -2, 8, -7, 4)

print("Positive numbers: ")
for (n in numbers) {
    if (n < 0) continue  // skip negative numbers
    print("$n ")
}
// Positive numbers: 1 5 8 4

Labels — break and continue in Nested Loops #

By default, break and continue only affect the innermost loop. To affect an outer loop, use labels.

// ANTI-PATTERN: trying to exit an outer loop with a boolean flag
var found = false
for (i in 1..5) {
    for (j in 1..5) {
        if (i * j == 12) {
            found = true
            break  // only exits the inner loop!
        }
    }
    if (found) break  // needs a second break for the outer loop
}

// CORRECT: use a label to exit the outer loop directly
outerLoop@ for (i in 1..5) {
    for (j in 1..5) {
        if (i * j == 12) {
            println("Found: $i × $j = 12")
            break@outerLoop  // exits the labeled loop directly
        }
    }
}
// Found: 3 × 4 = 12

Labels also work with continue:

outerLoop@ for (i in 1..3) {
    for (j in 1..3) {
        if (j == 2) continue@outerLoop  // go to the next iteration of the outer loop
        println("i=$i, j=$j")
    }
}
// i=1, j=1
// i=2, j=1
// i=3, j=1

repeat — Simple Iteration a Set Number of Times #

For repeating something N times without needing a counter variable, repeat is the cleanest choice:

// ANTI-PATTERN: for used only for counting
for (i in 1..5) {
    println("Hello!")
}

// CORRECT: repeat if you don't need the counter value
repeat(5) {
    println("Hello!")
}

// repeat provides the index if needed
repeat(5) { index ->
    println("Iteration $index")
}
// Iteration 0
// Iteration 1
// ...
// Iteration 4

Nested Loops #

Loops inside loops are useful for working with two-dimensional data structures like matrices, tables, or combinations.

// Multiplication table
for (i in 1..5) {
    for (j in 1..5) {
        print("%4d".format(i * j))
    }
    println()
}
//    1   2   3   4   5
//    2   4   6   8  10
//    3   6   9  12  15
//    4   8  12  16  20
//    5  10  15  20  25
// Finding element pairs whose sum equals a target
val numbers = listOf(1, 3, 5, 7, 9)
val target = 10

for (i in numbers.indices) {
    for (j in i + 1 until numbers.size) {
        if (numbers[i] + numbers[j] == target) {
            println("${numbers[i]} + ${numbers[j]} = $target")
        }
    }
}
// 1 + 9 = 10
// 3 + 7 = 10
Nested loops more than two levels deep are usually a sign the code needs refactoring — extract the inner loop into its own function, or consider a different approach. Nested complexity makes code hard to read and understand.

Functional Iteration — A More Expressive for Alternative #

Kotlin encourages using higher-order functions as an alternative to manual loops for common collection operations. The functional approach is usually more expressive, safer from off-by-one errors, and easier to combine.

flowchart TD
    A[Loop Requirement] --> B{Purpose?}
    B -- Do something\nfor each element --> C["forEach { }"]
    B -- Transform\neach element --> D["map { }"]
    B -- Filter elements --> E["filter { }"]
    B -- Accumulate\ninto one value --> F["reduce { } / fold { }"]
    B -- Find one element --> G["find { } / first { }"]
    B -- Check a condition\nacross all elements --> H["all { } / any { } / none { }"]
    B -- Need the index\nin each iteration --> I["forEachIndexed { }"]
    B -- Loop N times\nwithout a collection --> J["repeat(N) { }"]
    B -- Unknown\ndynamic condition --> K["while / do-while"]

forEach and forEachIndexed #

val students = listOf("Budi", "Sari", "Ahmad", "Rina")

// forEach — without index
students.forEach { name ->
    println("Hello, $name!")
}

// forEachIndexed — with index
students.forEachIndexed { i, name ->
    println("${i + 1}. $name")
}
// 1. Budi
// 2. Sari
// 3. Ahmad
// 4. Rina

map — Transform Every Element #

val prices = listOf(50_000, 75_000, 120_000, 30_000)

// ANTI-PATTERN: manual loop for transformation
val discountedPrices = mutableListOf<Int>()
for (p in prices) {
    discountedPrices.add((p * 0.8).toInt())
}

// CORRECT: map is more concise and safer
val discountedPrices = prices.map { (it * 0.8).toInt() }

println(discountedPrices)  // [40000, 60000, 96000, 24000]

filter — Filtering Elements #

val scores = listOf(55, 78, 90, 42, 85, 67, 91, 38)

// Manual loop
val passed = mutableListOf<Int>()
for (n in scores) {
    if (n >= 70) passed.add(n)
}

// Idiomatic
val passed = scores.filter { it >= 70 }
println(passed)  // [78, 90, 85, 91]

Combining Operations — Method Chaining #

The real power of the functional approach is how easily operations combine:

data class Product(val name: String, val price: Int, val stock: Int)

val products = listOf(
    Product("Laptop",    15_000_000, 5),
    Product("Mouse",        250_000, 0),
    Product("Keyboard",     500_000, 12),
    Product("Monitor",    4_000_000, 3),
    Product("Headset",      800_000, 0),
)

// Show available products, sorted from cheapest, showing name and price
val available = products
    .filter { it.stock > 0 }
    .sortedBy { it.price }
    .map { "${it.name}: Rp${\"%,d\".format(it.price)}" }

available.forEach { println(it) }
// Keyboard: Rp500,000
// Monitor: Rp4,000,000
// Laptop: Rp15,000,000

Compare with the equivalent manual loop — far more code and more bug-prone:

// ANTI-PATTERN: manual loop for the same thing
val available = mutableListOf<Product>()
for (p in products) {
    if (p.stock > 0) available.add(p)
}
available.sortBy { it.price }
val result = mutableListOf<String>()
for (p in available) {
    result.add("${p.name}: Rp${\"%,d\".format(p.price)}")
}
for (s in result) println(s)

reduce and fold — Accumulation #

val numbers = listOf(1, 2, 3, 4, 5)

// reduce: accumulation starts from the first element
val sum = numbers.reduce { acc, value -> acc + value }
println(sum)  // 15

val product = numbers.reduce { acc, value -> acc * value }
println(product)  // 120

// fold: accumulation with an initial value
val sumWithOffset = numbers.fold(100) { acc, value -> acc + value }
println(sumWithOffset)  // 115

// sumOf — specifically for sums (more expressive than reduce)
val total = numbers.sumOf { it }
println(total)  // 15

Choosing the Right Loop Type #

SituationBest Choice
Iterate a collection/range without modificationfor or forEach
Transform every elementmap
Filter elementsfilter
Accumulate into one valuereduce / fold / sumOf
Iterate with an indexforEachIndexed or for + withIndex()
Repeat N times without a counterrepeat(N)
Dynamic condition, uncertain iteration countwhile
At least one execution, then check the conditiondo-while
Early exit from nested loopsfor + label + break@label

Summary #

  • for in Kotlin is for-each — there’s no for (i=0; i<n; i++) form. Use ranges (1..5, 0 until n), withIndex(), or indices for the same purposes.
  • while for dynamic conditions — choose while when the iteration count isn’t known in advance and depends on a condition that changes during execution.
  • do-while guarantees at least one execution — useful for the “try first, check later” pattern, like asking for user input until it’s valid.
  • repeat(N) for fixed-count repetition — cleaner than for (i in 1..N) when the counter value isn’t needed.
  • Labels for break/continue in nested loopsbreak@label or continue@label affects the labeled loop, not just the innermost one.
  • Prefer the functional approach for collectionsforEach, map, filter, reduce, and their combinations are more expressive, safer, and easier to compose than manual loops.
  • Avoid nested loops more than two levels deep — extract into functions or consider a different approach. Three levels of nesting can almost always be simplified.
  • map isn’t for side effects — use forEach if the goal is an action (printing, saving to DB). Use map if the goal is producing a new collection from a transformation.

← Previous: Conditional Statements   Next: Functions →

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