1. Introduction
Sometimes it seems that while is a “universal loop”, and you can stop there. But in real programs (even the simplest console ones) you often encounter a scenario where an action must happen at least once. For example, we must ask the user for a command or a number at least once—otherwise we simply have no data to check anything.
And here while starts forcing you to write an “awkward ritual”: first perform the action once before the loop, and then repeat the same action inside the loop. It’s not fatal, but it’s annoying—like when you check twice whether you locked the door, and doubt it both times.
do-while exists precisely for such cases: “run the body at least once, and only then look at the condition”.
From the Kotlin language point of view: in while, the condition is checked before the body, and in do-while it’s checked after the body, so the do-while body executes at least once.
do-while syntax and a “human” mental model
When you see do-while for the first time, it looks a bit unusual: the condition is not on top, but at the bottom. But the logic is very simple: first we take a step, then we ask ourselves “one more time?”.
Syntax:
do {
// loop body
} while (condition)
This is literally read like a sentence: “Do { … } and repeat while condition is true”.
It’s important to remember one rule that will later save you from confusion: in do-while, the condition controls whether the next iteration repeats, not whether the first one is allowed. The first iteration always happens—because the “do first” part has already occurred.
2. while and do-while on the same task
It’s very useful to see the difference not “in theory”, but in the very same scenario. Let’s take this task: the user must type the word "exit" to finish. We must ask at least once.
while variant with duplication
fun main() {
print("Enter a command (exit to quit): ")
var cmd = readln().trim().lowercase()
while (cmd != "exit") {
print("Enter a command (exit to quit): ")
cmd = readln().trim().lowercase()
}
println("Finished.") // Finished.
}
This code works, but note: the line with print(...) and the line that reads readln() occur twice. Beginners sometimes say: “So what”. And then comes the moment when you change the prompt text and forget to change it in one place. And the program starts behaving as if it has a split personality.
do-while variant without duplicates
fun main() {
var cmd: String
do {
print("Enter a command (exit to quit): ")
cmd = readln().trim().lowercase()
} while (cmd != "exit")
println("Finished.") // Finished.
}
Same meaning, but the “duplication ritual” is gone. That’s the main practical bonus of do-while.
3. Re-entering a number without duplication
Now we’ll do exactly what do-while is especially loved for in console programs: robust input “until the user enters it properly”.
Suppose we need a number from 1 to 5. The user can type anything: 3, " 3 ", "three", an empty string, 999. Our program should stay calm and simply repeat the prompt.
Here we use a familiar pattern: toIntOrNull() returns Int?, meaning either a number or null.
Reading a number in the range 1..5 via do-while
fun main() {
var number: Int? = null
do {
print("Enter an integer from 1 to 5: ")
val raw = readln().trim()
val parsed = raw.toIntOrNull()
number = if (parsed != null && parsed in 1..5) parsed else null
if (number == null) println("Invalid.") // Invalid.
} while (number == null)
println("Accepted: $number") // Accepted: 4
}
There is an important idea here that makes the code readable: the variable number stores the “accepted” value. Until it’s accepted, it’s null. The loop repeats while number == null.
4. Scope in do-while
When you write do-while, a perfectly normal question arises: “Where do I declare the variable that I check in the condition?”. The answer: so that it is visible both in the body and in the while (...) condition.
That’s why this pattern is extremely common:
fun main() {
var cmd: String
do {
cmd = readln().trim()
} while (cmd != "exit")
}
If you declare val cmd inside the do { ... } block, then outside (in while (cmd != ...)) it will no longer be visible—and the compiler will say “I don’t know that name”. This is not Kotlin being picky; it’s the usual scope rule: a name lives inside the block where it is declared.
To reinforce that, here is an example of “what not to do” (it won’t compile):
fun main() {
do {
val cmd = readln().trim()
} while (cmd != "exit") // Error: cmd is not visible here
}
5. Useful nuances of do-while
Mini-diagram: how do-while runs step by step
To get less confused, it helps to keep a small execution diagram in mind. Let’s imagine we have a loop that asks the user to re-enter a number (while number == null).
flowchart TD
A["START"] --> B["do { ... } executes (at least 1 time)"]
B --> C{"while (...) condition?"}
C -->|true| B
C -->|false| D["loop ends"]
D --> E["END"]
Compare it with while: there the check is before the body, so the “0 iterations” scenario is possible. With do-while it is not: the first iteration always happens.
How to write a readable condition
It’s easy to turn do-while into a monster if you write a condition inside while (...) with five checks, parentheses, &&, ||, and attempts to “fit everything into one line”. At this stage, it helps to remember: readability is part of correctness.
If the condition becomes heavy, it’s easier to do this: compute “accepted/not accepted” into a separate variable inside the loop body, and then put that variable into while.
Here’s an example with the same age input, but a bit more “conversational”:
fun main() {
var age: Int? = null
do {
print("Age (0..120): ")
val parsed = readln().trim().toIntOrNull()
val ok = parsed != null && parsed in 0..120
age = if (ok) parsed else null
if (!ok) println("Enter an integer from 0 to 120.")
} while (age == null)
println("Age accepted: $age") // Age accepted: 42
}
The code got one line longer, but it’s easier for the brain.
Micro-walkthrough by iterations: what happens to variables
Sometimes it’s useful to “run” the loop in your head as if you were debugging the program manually. Let’s take age input and assume the user entered: "abc", then "-5", then "30".
Inside the body, on each iteration, it looks roughly like this:
| Iteration | raw | parsed | age after assignment | Condition age == null |
|---|---|---|---|---|
| 1 | |
|
|
true (repeat) |
| 2 | |
|
|
true (repeat) |
| 3 | |
|
|
false (exit) |
That is, do-while here does very clear work: “try to accept a value until it works”.
6. Questionnaire v0.4: name and age
So the examples are not just a set of separate snippets, let’s keep evolving one mini-application. Let it be a “questionnaire”: the user enters a name and an age. We don’t make functions (except main) and we don’t use arrays—everything at the current-day level.
The idea is: the name must not be empty (after trim()), and the age must be an integer from 0 to 120. We want to “bother” the user as little as possible, but still not accept garbage.
Step 1: reliable name input
fun main() {
var name: String
do {
print("Enter your name: ")
name = readln().trim()
if (name.isEmpty()) println("Name must not be empty.") // Name must not be empty.
} while (name.isEmpty())
println("Hello, $name!") // Hello, Anna!
}
Note: here we don’t have null, because readln() always returns a string. It’s enough to check isEmpty() after trim().
Step 2: reliable age input
Now let’s add age. Here we use Int? because toIntOrNull() can return null.
fun main() {
var name: String
do {
print("Enter your name: ")
name = readln().trim()
if (name.isEmpty()) println("Name must not be empty.")
} while (name.isEmpty())
var age: Int? = null
do {
print("Enter age (0..120): ")
val raw = readln().trim()
val parsed = raw.toIntOrNull()
age = if (parsed != null && parsed in 0..120) parsed else null
if (age == null) println("Age was entered incorrectly.") // Age was entered incorrectly.
} while (age == null)
println("User: $name, age: $age") // User: Anna, age: 25
}
Technically, we could store age as an Int and separately keep a “valid/invalid” flag, but at this stage Int? is often simpler: null = “not accepted yet”.
7. Typical mistakes when working with do-while
Mistake #1: expecting that the loop might not run even once.
That’s a habit from while. In do-while the body will always run at least once, even if the condition is initially false. Therefore, do-while is not suitable for situations where the action might not be needed at all. If “0 iterations is fine” is important, while is often a better fit.
Mistake #2: declaring a variable inside do {} and using it in while (...).
This is the classic scope problem. If a variable is declared inside a block, it is not visible outside, and the condition won’t compile. Almost always, the variable used in the condition is declared before the loop so it is available in both places.
Mistake #3: using toInt() instead of toIntOrNull() in a re-entry scenario.
toInt() on input like "abc" will terminate the program with an exception, but in learning scenarios we need the opposite—gently say “invalid” and repeat. For robust input, toIntOrNull() is better because it turns non-numeric input into null.
Mistake #4: forgetting to normalize the string (trim()) before checks.
The user can easily type " exit " or " 25 ". Without trim(), such values often won’t match what you expect, and the program will stubbornly act as if the user is wrong, even though they just added spaces. This is especially noticeable with commands ("exit") and numbers.
Mistake #5: an overly complex condition in while (...) and losing the meaning.
If you write a half-screen expression at the bottom of the loop, you are almost guaranteed to make a mistake in parentheses or logic one day. At this level, it’s better to compute “accepted/not accepted” inside the body into a separate val ok and then control repetition via a clear check like while (!ok) or while (age == null).
Mistake #6: mixing different roles in a single variable.
For example, trying to store both the “raw input string”, the “number”, and “whether the value was accepted” in one var x. That creates confusion, and then bugs that are hard to explain even to yourself. Better: raw for the string, parsed for the parsing result, and a separate variable for the accepted value (age) or for the flag (ok).