while and do-while Loops

A while loop repeats a block of code for as long as a condition stays true, checking that condition before every iteration. A do-while loop is its close cousin: it also repeats while a condition holds, but it checks the condition after running the body, so the body always executes at least once. Kotlin keeps both forms from Java and C-family languages, but with a few refinements you need to know about — including a genuinely unique scoping rule for do-while. Mastering when to reach for which loop, and how Kotlin’s val/var and null-safety rules interact with loop counters, is essential for writing correct, idiomatic control flow.

Overview / How It Works

Both loops evaluate a Boolean expression to decide whether another iteration should run. Kotlin is strict about this: the condition must be a real Boolean, not an Int, not a nullable Boolean?, and not some ‘truthy’ value the way some scripting languages allow. If you have a Boolean? — for example, the result of a nullable check — you must resolve it to a non-null Boolean first, typically with the Elvis operator (?:) or a null check, before it can drive a loop.

The key structural difference is when the condition is checked:

  • while is a pre-test (head-checked) loop — the condition is evaluated first, and if it’s false immediately, the body never runs at all.
  • do-while is a post-test (tail-checked) loop — the body runs first, and the condition is only evaluated afterward, so the body is guaranteed to execute at least once, no matter what.

Another thing worth internalizing early: unlike if and when in Kotlin, which can be used as expressions that produce a value, while and do-while are always statements. They evaluate to Unit and can never appear on the right-hand side of an assignment. If a loop needs to produce a result, you accumulate that result into a var declared before the loop starts, then read that variable once the loop finishes.

Under the hood, the compiler translates both loops into JVM bytecode using conditional jump instructions (roughly, a comparison followed by a branch back to the top of the loop body) — there’s no hidden allocation, no iterator object, and no boxing overhead beyond whatever your loop body itself does. This makes while/do-while the right tool when you’re looping based on a condition that doesn’t map neatly onto a range or a collection (which is what Kotlin’s for loop is built for).

Syntax

while (condition) {
    // loop body — runs repeatedly while condition is true
}

do {
    // loop body — runs at least once
} while (condition)
Part Meaning
condition A non-null Boolean expression checked on every pass
while body Checked before running — may execute zero times
do ... while body Checked after running — always executes at least once

Examples

Example 1: Counting Down with while

fun main() {
    var count = 5
    while (count > 0) {
        println("Count: $count")
        count--
    }
    println("Liftoff!")
}

Output:

Count: 5
Count: 4
Count: 3
Count: 2
Count: 1
Liftoff!

count must be a var because it’s reassigned on every pass with count--. The condition count > 0 is checked before each iteration; once count reaches 0, the loop exits and control falls through to the final println.

Example 2: Guaranteed First Run with do-while

fun main() {
    var attempts = 0
    val maxAttempts = 3
    do {
        attempts++
        println("Attempt $attempts")
    } while (attempts < maxAttempts)
    println("Done after $attempts attempts")
}

Output:

Attempt 1
Attempt 2
Attempt 3
Done after 3 attempts

This models a classic retry pattern: you always want to try at least once before checking whether you should try again. Compare that to a while loop whose condition is already false:

fun main() {
    val ready = false
    var checks = 0
    while (ready) {
        checks++
        println("Checking...")
    }
    println("Checks performed: $checks")
}

Output:

Checks performed: 0

Because ready is false from the start, the while body never runs — checks stays at its initial value. A do-while with the same condition would still have run its body once. This is the whole reason the two loop forms exist as separate constructs.

Example 3: Kotlin's do-while Scope Quirk

fun main() {
    var index = 0
    val numbers = listOf(4, 8, 15, 16, 23, 42)
    do {
        val current = numbers[index]
        println("Value at $index: $current")
        index++
    } while (index < numbers.size && current < 40)
}

Output:

Value at 0: 4
Value at 1: 8
Value at 2: 15
Value at 3: 16
Value at 4: 23
Value at 5: 42

Notice that current is declared with val inside the do block, yet it's still readable in the while condition. This is a genuine Kotlin-specific rule: a do-while loop's body and its trailing condition share one scope, so variables declared in the body remain visible to the condition check. Java does not allow this — in Java, a variable declared inside a do block's braces is out of scope by the time the while (...) is evaluated. It's a small detail, but it's exactly the kind of thing that surprises Java developers reading Kotlin code for the first time.

Example 4: Draining a Queue with while

fun main() {
    val tasks = mutableListOf("compile", "test", "package", "deploy")
    var processed = 0
    while (tasks.isNotEmpty()) {
        val task = tasks.removeAt(0)
        println("Processing: $task")
        processed++
    }
    println("Processed $processed tasks")
}

Output:

Processing: compile
Processing: test
Processing: package
Processing: deploy
Processed 4 tasks

This is a very common real-world shape: loop while a mutable collection has elements left, removing one each pass. Note that tasks is declared with val — that's fine, because val only prevents reassigning the tasks reference to a different list. It does nothing to stop you from mutating the list's contents with removeAt. The reference stays pointed at the same MutableList object; only what's inside it changes.

How It Works Step by Step

For a while loop, the JVM-level execution order is:

  • 1. Evaluate condition.
  • 2. If false, skip the body entirely and continue after the loop.
  • 3. If true, execute the body once.
  • 4. Jump back to step 1.

For a do-while loop, the order flips:

  • 1. Execute the body once, unconditionally.
  • 2. Evaluate condition (which can reference variables just declared in the body).
  • 3. If true, jump back to step 1.
  • 4. If false, continue after the loop.

In both forms, a break exits the nearest enclosing loop immediately, and a continue skips straight to the next condition check — those two keywords work identically in while and do-while, and identically to how they work in Kotlin's for loop.

Common Mistakes

1. Declaring the loop counter with val

A loop counter has to change, so it cannot be a val. The compiler rejects any reassignment of a val:

val i = 0
while (i < 5) {
    println(i)
    i++
}

This fails to compile with 'Val cannot be reassigned' on the i++ line. Fix it by declaring the counter with var:

var i = 0
while (i < 5) {
    println(i)
    i++
}

Output:

0
1
2
3
4

2. Forgetting to update the condition variable

If nothing inside the body ever changes the value the condition depends on, the loop never terminates:

var count = 0
while (count < 5) {
    println("Looping")
}

This compiles just fine — an infinite loop is not a compile-time error — but it will never stop printing "Looping" at runtime, since count is never incremented. Always make sure some statement in the body moves the loop toward its exit condition:

var count = 0
while (count < 5) {
    println("Looping")
    count++
}

Output:

Looping
Looping
Looping
Looping
Looping

3. Using do-while when the body assumes non-empty state

Because do-while always runs its body at least once, it's the wrong choice whenever the body assumes some precondition — like a non-empty collection — that might not hold on the very first pass:

fun main() {
    val queue = mutableListOf()
    do {
        val item = queue.removeAt(0)
        println("Processing $item")
    } while (queue.isNotEmpty())
}

Output: Throws IndexOutOfBoundsException at queue.removeAt(0) — the do-while body runs unconditionally even though queue starts out empty, so there is no element at index 0 to remove.

Swap in a plain while loop, which checks the condition first and simply never enters the body when the queue is already empty:

fun main() {
    val queue = mutableListOf()
    while (queue.isNotEmpty()) {
        val item = queue.removeAt(0)
        println("Processing $item")
    }
    println("Queue empty, nothing to process")
}

Output:

Queue empty, nothing to process

Best Practices

  • Default to while; reach for do-while only when you specifically need the body to run before the first condition check (retry logic, menu prompts, 'read then validate' patterns).
  • Prefer a for loop over while whenever you're simply iterating a range or a collection — reserve while/do-while for condition-driven looping that doesn't map onto a fixed sequence.
  • Always declare the loop's mutable state with var, and double-check that some statement in the body actually changes it.
  • Remember that val on a mutable collection only locks the reference, not its contents — you can still add/removeAt/mutate freely.
  • When a nullable value drives the condition, resolve it to a non-null Boolean with ?: or a null check before the loop, rather than fighting the compiler.
  • Prefer while (true) { ... if (done) break } over convoluted boolean flag juggling when a loop's exit condition is naturally decided partway through the body.
  • Keep loop bodies short and readable; extract a named function if the body grows past a few responsibilities.

Practice Exercises

  • Write a program that starts with a variable at 100 and repeatedly halves it (integer division) with a while loop, printing each value, until it reaches 0.
  • Using a do-while loop, simulate a simple PIN check: start with an attempts counter at 0, increment it inside the loop, and keep looping while attempts is less than 3 and a fixed guess (e.g. 1234) doesn't equal a fixed correct PIN (e.g. 5678). Print whether access was granted.
  • Given a MutableList of integers, write a while loop that removes and sums elements from the front of the list until the list is empty or the running sum exceeds 50, then print the final sum.

Summary

  • while checks its condition before each pass and may never run its body; do-while checks after, so its body always runs at least once.
  • Both are statements, not expressions — they always produce Unit, never a value you can assign directly.
  • Kotlin uniquely allows a do-while's condition to see variables declared inside its own body, because the body and condition share one scope — Java doesn't allow this.
  • Loop counters and any variable reassigned inside the loop must be declared var; a val cannot be reassigned and will fail to compile.
  • A val reference to a mutable collection can still have its contents changed inside a loop — only reassignment of the variable itself is blocked.
  • Choose do-while deliberately: if the body assumes preconditions that might not hold initially (like a non-empty collection), a plain while is almost always the safer choice.