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:
whileis a pre-test (head-checked) loop — the condition is evaluated first, and if it’sfalseimmediately, the body never runs at all.do-whileis 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 fordo-whileonly when you specifically need the body to run before the first condition check (retry logic, menu prompts, 'read then validate' patterns). - Prefer a
forloop overwhilewhenever you're simply iterating a range or a collection — reservewhile/do-whilefor 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
valon a mutable collection only locks the reference, not its contents — you can stilladd/removeAt/mutate freely. - When a nullable value drives the condition, resolve it to a non-null
Booleanwith?: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
100and repeatedly halves it (integer division) with awhileloop, printing each value, until it reaches0. - Using a
do-whileloop, simulate a simple PIN check: start with an attempts counter at0, increment it inside the loop, and keep looping while attempts is less than3and a fixed guess (e.g.1234) doesn't equal a fixed correct PIN (e.g.5678). Print whether access was granted. - Given a
MutableListof integers, write awhileloop that removes and sums elements from the front of the list until the list is empty or the running sum exceeds50, then print the final sum.
Summary
whilechecks its condition before each pass and may never run its body;do-whilechecks 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; avalcannot be reassigned and will fail to compile. - A
valreference to a mutable collection can still have its contents changed inside a loop — only reassignment of the variable itself is blocked. - Choose
do-whiledeliberately: if the body assumes preconditions that might not hold initially (like a non-empty collection), a plainwhileis almost always the safer choice.
