Control Flow (if, for, while)
Reviewed & published by Brayan K
Control flow is how a program decides what to do and how often to do it — Kotlin's if, for, and while let you branch on conditions and repeat work.
Part of the free Kotlin course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
A standout twist: in Kotlin if is an expression that returns a value, so it replaces the ternary operator entirely.
What You'll Learn in This Lesson
- Using if/else as both a statement and an expression
- Looping over ranges, collections, and indices with for
- while and do/while loops and how they differ
- break and continue to control a loop's flow
- Labels for breaking out of nested loops
- Why Kotlin needs no ternary operator
1️⃣ if/else — Statement and Expression
You can use if the familiar way — to run one block or another. But Kotlin goes further: if is an expression, so it produces a value you can assign. The value is the last expression of whichever branch runs. That is why Kotlin has no ? : ternary — if/else already covers it.
fun main() {
val a = 7
val b = 12
// Used as a STATEMENT (classic if/else):
if (a > b) {
println("a is bigger")
} else {
println("b is bigger") // -> b is bigger
}
// Used as an EXPRESSION — if RETURNS a value in Kotlin.
// The last line of each branch is the result.
val max = if (a > b) a else b
println("max = $max") // -> max = 12
// There is no ternary (?:) in Kotlin — if/else IS the ternary.
val label = if (a % 2 == 0) "even" else "odd"
println("$a is $label") // -> 7 is odd
}2️⃣ for Loops — Ranges, Collections, Indices
Kotlin's for loop always iterates over something: a range like 1..3, a collection like a list, or an indexed view via withIndex(). There is no C-style for (i = 0; i < n; i++) — ranges replace it and read far more clearly.
fun main() {
// Iterate over a RANGE (1, 2, 3):
for (i in 1..3) {
println("count $i")
}
// Iterate over a COLLECTION directly:
val fruits = listOf("apple", "pear", "plum")
for (fruit in fruits) {
println(fruit)
}
// Iterate with INDEX + value using indices / withIndex:
for ((index, fruit) in fruits.withIndex()) {
println("$index -> $fruit")
}
}3️⃣ while, do/while, break, and continue
A while loop repeats as long as its condition is true, checking before each pass. A do/while loop runs the body once and checks after. Inside any loop, break exits immediately and continue jumps to the next pass.
fun main() {
// while: checks the condition BEFORE each pass.
var n = 3
while (n > 0) {
println("n = $n")
n--
}
// do/while: runs the body ONCE, then checks.
var k = 0
do {
println("k = $k")
k++
} while (k < 2)
}Now the same loop-control tools, plus a label to break out of an outer loop from deep inside a nested one:
fun main() {
// break stops the loop; continue skips to the next pass.
for (i in 1..5) {
if (i == 3) continue // skip 3
if (i == 5) break // stop before 5
println(i)
}
// Labels let you break an OUTER loop from inside an inner one.
outer@ for (row in 1..3) {
for (col in 1..3) {
if (row + col == 4) break@outer
println("$row,$col")
}
}
}Your turn. Fill in the ___ blanks, then run and compare.
// 🎯 YOUR TURN — replace each ___ then run main()
fun main() {
val score = 85
// 1) if as an EXPRESSION: "pass" if score >= 60, else "fail"
val result = if (score >= 60) "pass" else ___ // 👉 "fail"
// 2) loop the numbers 1 through 3 with a range
for (i in 1.._) { // 👉 1..3
print(i)
}
println()
println(result)
// ✅ Expected:
// 123
// pass
}Mini-Challenge: FizzBuzz
The legendary interview warm-up. Combine a for loop, if/else, and the modulo operator %.
// 🎯 MINI-CHALLENGE: FizzBuzz (the classic).
// Save as FizzBuzz.kt and run main().
//
// Loop i from 1 to 5. For each number:
// • print "Fizz" if i is divisible by 3
// • print "Buzz" if i is divisible by 5
// • print "FizzBuzz" if divisible by BOTH 3 and 5
// • otherwise print the number itself
//
// Hint: use if/else if/else and the % operator.
//
// ✅ Expected output:
// 1
// 2
// Fizz
// 4
// Buzz
fun main() {
for (i in 1..5) {
// your code here
}
}Common Errors (and the fix)
- ❌ val x = if (a > b) a with no else when used as a value. ✅ An if expression needs an else so every path yields a value.
- ❌ Writing a C-style for (i = 0; i < n; i++). ✅ Kotlin uses ranges: for (i in 0 until n).
- ❌ Forgetting to change the variable inside a while (e.g. no n--) creates an infinite loop. ✅ Always move toward the exit condition.
- ❌ Using break expecting it to leave the outer loop. ✅ Use a label: break@outer.
Pro Tips
- 💡 Prefer for over while whenever you know the collection or range — it is shorter and can't loop forever by accident.
- 💡 Use withIndex() when you need both the position and the value, instead of indexing manually.
- 💡 Reach for when (Kotlin's switch) once an if/else if chain grows past two or three branches.
📋 Quick Reference — Control Flow
| Construct | Kotlin Syntax |
|---|---|
| if statement | if (c) { ... } else { ... } |
| if expression | val m = if (c) a else b |
| for over range | for (i in 1..3) { ... } |
| for with index | for ((i, v) in list.withIndex()) |
| while | while (c) { ... } |
| do/while | do { ... } while (c) |
| labeled break | break@outer |
🎉 Lesson Complete!
- ✅ if is an expression — it returns a value and replaces the ternary
- ✅ for iterates over ranges, collections, and indices
- ✅ while checks first; do/while runs once then checks
- ✅ break exits a loop, continue skips to the next pass
- ✅ Labels like outer@ control nested loops
Practice quiz
In Kotlin, can if be used as an expression that returns a value?
- No, if is always a statement
- Only inside loops
- Yes, if/else returns the last expression of the chosen branch
- Only with a return keyword
Answer: Yes, if/else returns the last expression of the chosen branch. if is an expression in Kotlin, so val max = if (a > b) a else b works.
Why does Kotlin have no ternary (cond ? a : b) operator?
- Because if/else is already an expression that returns a value
- Because ternary is considered unsafe
- Because when replaces it
- It does have a ternary operator
Answer: Because if/else is already an expression that returns a value. if/else is an expression, so it covers what a ternary does in other languages.
When if is used as an expression assigned to a variable, the else branch is:
- Optional
- Forbidden
- Replaced by when
- Required so every path produces a value
Answer: Required so every path produces a value. The compiler must know what value to produce in every case, so else is required.
What does the range 1..3 produce when looped?
- 1, 2 (upper excluded)
- 1, 2, 3 (both endpoints included)
- 0, 1, 2
- 2, 3
Answer: 1, 2, 3 (both endpoints included). The .. operator builds a closed range that includes both endpoints.
What does 1 until 3 produce?
- 1, 2 (upper bound excluded)
- 1, 2, 3
- 0, 1, 2, 3
- 2, 3
Answer: 1, 2 (upper bound excluded). until builds a half-open range that excludes the upper bound.
Which loop checks its condition AFTER running the body once?
- while
- for
- do/while
- repeat
Answer: do/while. do/while runs the body once, then checks the condition.
Inside a loop, what does continue do?
- Exits the loop entirely
- Skips to the next iteration
- Restarts the whole loop
- Pauses execution
Answer: Skips to the next iteration. continue jumps to the next pass; break exits the loop.
How do you break out of an OUTER loop from inside a nested loop?
- Use break twice
- Use return
- It is not possible
- Use a label, e.g. break@outer
Answer: Use a label, e.g. break@outer. A label like outer@ lets you write break@outer to exit the outer loop.
Which is a valid Kotlin for loop over a list named fruits?
- for (i = 0; i < fruits.size; i++) { }
- for (fruit in fruits) { }
- foreach fruit in fruits { }
- for fruit of fruits { }
Answer: for (fruit in fruits) { }. Kotlin's for always iterates over something; there is no C-style for.
What does fruits.withIndex() give you in a for loop?
- Only the indices
- Only the values reversed
- Both the index and the value of each element
- A new sorted list
Answer: Both the index and the value of each element. withIndex() yields (index, value) pairs you can destructure.
Continue this course
- Previous: Control Flow & when
- Next: Ranges & Progressions — 1..10, until, downTo, step, and in/!in membership checks
- Quick reference: Kotlin cheat sheet
Frequently asked questions
Why does Kotlin have no ternary operator (cond ? a : b)?
Because it doesn't need one. In Kotlin if/else is an expression, meaning it evaluates to a value, so val max = if (a > b) a else b does exactly what a ternary does in other languages — only more readable. The result of each branch is its last expression. The same idea applies to when, which is Kotlin's powerful replacement for switch and also returns a value.
What is the difference between a range like 1..3 and 1 until 3?
The .. operator builds a closed range that INCLUDES both endpoints, so 1..3 is 1, 2, 3. The until keyword builds a half-open range that EXCLUDES the upper bound, so 1 until 3 is just 1, 2. until is handy when looping over indices, where you usually want 0 until size. You'll explore ranges and progressions in depth in the next lesson.
When should I use while versus for?
Use a for loop when you know what you are iterating over — a range, a list, or a sequence of indices. Use while when you loop until some condition changes and you don't have a fixed collection, for example reading input until a sentinel value appears. Use do/while in the rare case where the body must run at least once before the condition is checked.
What are labels like outer@ used for with break and continue?
By default break and continue affect only the innermost loop that contains them. A label such as outer@ placed before a loop lets you write break@outer or continue@outer from inside a nested loop to control the outer loop directly. This avoids awkward flag variables when you need to exit several levels of nesting at once.