1. Introduction
Imagine you are writing a program that asks the user to enter their age. The user enters "abc". You say "invalid". And… what next? If you only have if, the program simply reaches the end and exits, leaving the user with a feeling of “oh well then.” Loops exist for exactly this: to teach a program to repeat an action until the desired result is reached — or while the condition is true.
In real life, loops show up everywhere: repeated input, counting “from 1 to N,” accumulating a sum, gradually moving toward a goal. And yes, loops are the main tool with which you can accidentally create infinity (programmers sometimes like eternity too, but the compiler does not always agree).
2. while: syntax and mental model
The while loop reads very naturally: “while the condition is true — do it”. In Kotlin it looks like this:
while (condition) {
// loop body
}
The main feature: the condition is checked before entering the body. This means that if the condition is immediately false, the body will not run even once — and that is not a “bug,” but useful behavior. That is why while is ideal for “run while it makes sense” situations.
while is like a bouncer at a club entrance: “Do you have a ticket?” (the condition). If you do not, you will not get in at all — not for a single iteration.
A small flowchart to see the program’s movement with your own eyes:
flowchart TD
A[Start] --> B{condition?}
B -- no --> E[Exit loop]
B -- yes --> C[Loop body]
C --> D[End of iteration]
D --> B
And yes: Kotlin is Kotlin, but while here is absolutely classic, “like everywhere,” with no surprises. Even in official imperative-style examples (where var and a loop are used), while looks exactly like this.
3. while patterns
Counter: three required parts
When beginners see a loop, the first thought is often: “Oh, repetition! Now I’ll repeat something… and it will stop by itself.” It will not. A loop needs your help.
A classic counter loop has three parts, and you can almost always see them directly in the code:
- initial value (before the loop),
- continuation condition (in while (...)),
- changing the variable so the condition eventually becomes false (inside the body).
The simplest example is printing numbers from 1 to 5:
fun main() {
var i = 1
while (i <= 5) {
println("i = $i") // i = 1, then 2, 3, 4, 5
i = i + 1
}
}
Note: the variable i is declared as var because we change it. If it were val, the compiler would reasonably say: “but you promised not to change it.”
Now an example with a boundary where the body might not execute even once. This is a very important “honesty check” for your understanding:
fun main() {
var i = 10
while (i <= 5) {
println("This will not be printed")
i = i + 1
}
println("The loop did not run even once") // The loop did not run even once
}
The condition i <= 5 is immediately false, so we never enter the loop. Sometimes this is exactly what you want: for example, “repeat while there are errors,” and there might be no errors at the start.
Accumulation: sum and maximum
The while loop is useful not only for printing “steps,” but also for accumulating a result. A typical scenario: “there are several numbers, we need to compute the sum.” While we do not yet have arrays and lists, we practice on a fixed number of values or “enter N times.”
Let’s start very small: the sum from 1 to 3.
fun main() {
var sum = 0
var i = 1
while (i <= 3) {
sum = sum + i
i = i + 1
}
println("sum = $sum") // sum = 6
}
Here the two variable roles are very clear: i is the “step,” sum is the “accumulator.” When you mix roles (for example, using one variable both as a counter and as a sum), the code starts to look like a joke that is funny only to the author.
Another useful pattern is finding the maximum among several entered values. Yes, later we will do this much more conveniently, but right now the “compare and update” logic is what matters.
Suppose we want to process 3 numbers (we assume the input is correct so we do not distract from the topic):
fun main() {
var max: Int? = null
var count = 0
while (count < 3) {
print("Enter a number: ")
val n = readln().trim().toInt()
if (max == null || n > max) max = n
count = count + 1
}
println("max = $max") // for example: max = 42
}
Here max is nullable (Int?) because there is no “maximum” before the first input. This is a good example that nullable types are not only for “scary null topics,” but sometimes simply an honest way to say: “the value will appear later.”
Infinite loop: why it happens and how to spot it
Time to talk about the main entertainment for a beginner programmer: “I ran the program and it hung.” In reality, it often did not hang — it just found the meaning of life and is now endlessly repeating the same thing.
The reason for an infinite loop is almost always the same: the condition does not become false. Either you forgot to change the variable, or you change the wrong one, or you change it “in the wrong direction.”
The most classic mistake:
fun main() {
var i = 1
while (i <= 5) {
println("i = $i")
// forgot i = i + 1
}
}
Here i is always 1, the condition is always true, and the program will print "i = 1" until you (or your operating system) say: “that’s enough.”
An even more devious version: you update the variable, but in a way that never makes the condition false:
fun main() {
var i = 1
while (i <= 5) {
println("i = $i")
i = i - 1
}
}
i goes to 0, -1, -2… and the condition i <= 5 remains true forever.
The practical “anti-infinity” rule sounds boring, but it works flawlessly: after writing while visually find a line inside the body that affects the condition. If you cannot find it, you almost certainly have a perpetual motion machine.
4. Repeated input and validation
Counters are great, but one of the most real-world reasons to love while is robust input. The user can enter spaces, an empty string, “forty two” in letters, and even "42.0" (while you wanted an Int). And if you use toInt(), the program can crash. So for the scenario “repeat input until a valid value,” we use the combination trim() + toIntOrNull().
readln() reads a whole line, and toIntOrNull() returns either a number or null if the string is not a number. This fits perfectly with while.
Example: ask for age (an integer from 0 to 130) and repeat until we get a normal result.
fun main() {
var age: Int? = null
while (age == null) {
print("Enter age (0..130): ")
val raw = readln().trim()
val parsed = raw.toIntOrNull()
if (parsed != null && parsed in 0..130) {
age = parsed
} else {
println("Invalid input. Please try again.")
}
}
println("Age accepted: $age") // for example: Age accepted: 25
}
Why this is convenient:
- We do not “crash” on bad input — because we do not use toInt() directly.
- The loop condition is very readable: “while the age has not been obtained.”
- Inside the loop, it is clearly visible what makes the input valid: the number must parse and fall within the range.
Notice an important detail: checking parsed in 0..130 is only possible if parsed != null. That is why we write parsed != null && ... This continues the same logic you have already used in conditions with && and ||.
5. Mini app: BudgetBuddy v0
Now we will assemble a small end-to-end scenario that will look like a “real program,” not a set of disconnected examples. Let it be BudgetBuddy v0: a mini assistant that asks you to enter three expenses and calculates the total. The main focus is the while loop and resilience to incorrect input without break/continue (we have not covered those yet).
The idea is: we need exactly 3 valid numbers. If the user enters nonsense, we do not move the counter and we ask again. This is a very important technique: the counter increments only on success.
fun main() {
var sum = 0
var entered = 0
while (entered < 3) {
print("Enter expense #${entered + 1} (integer >= 0): ")
val raw = readln().trim()
val value = raw.toIntOrNull()
if (value != null && value >= 0) {
sum = sum + value
entered = entered + 1
} else {
println("Invalid. An integer >= 0 is required.")
}
}
println("Sum of 3 expenses = $sum") // for example: Sum of 3 expenses = 1200
}
Notice how neatly the roles are distributed:
entered is responsible only for how many valid expenses we have accepted. If the input is bad, entered does not change, and we stay on the same “expense number.” This is “repeated input” in real life, without duplicating code.
And one more detail about readability: "№${entered + 1}" is a string template you already know. It makes the prompt friendly: the user understands they are entering “expense #2,” not just some abstract numbers into the void.
6. Typical mistakes when working with while
Mistake #1: you forgot to change the variable the condition depends on.
The most common cause of an infinite loop is that the variable in the condition (i, entered, age) remains unchanged. A useful habit: immediately after writing while (...), visually find a line in the body that eventually makes the condition false. If there is no such line, the loop is most likely infinite.
Mistake #2: you update “in the wrong direction,” and the condition will never become false.
Sometimes there is an update, but the logic is wrong: for example, in a while (i <= 5) loop you do i = i - 1. Formally, the program “changes something,” but meaningfully it moves into a region where the condition stays true forever. When you notice that the value is “running away” from the boundary, stop and check the direction/sign of the change.
Mistake #3: you mixed variable roles, and the loop stopped being understandable.
If you use one variable to count steps, store the sum, and also try to keep the “last input,” the code becomes hard to read and easy to break. A good beginner style is “one variable = one role”: separate counter, separate accumulator, separate parsed value.
Mistake #4: you use toInt() in an input loop and end up crashing the program.
toInt() is good when you are confident about the input format. But in “user input” tasks, confidence is usually not warranted. So for repeated input, it is better to use toIntOrNull() and a null check, as we did above: the program does not crash and can politely ask for the input again.
Mistake #5: you compare or check a range for a value that can be null.
A typical situation: val x = raw.toIntOrNull() and then an attempt to do if (x >= 0). The Kotlin compiler will not allow that (and it will be right), because x can be null. The correct order of thinking is: first “not null?”, then “does it fit the range?” This is especially important in loops, because an error in validation often turns into endless repetition or an inability to accept valid input.