Loop control is one of those quiet skills that separates tidy apps from spaghetti code. In Kotlin, the `break` and `continue` statements give you precise control over `for`, `while`, and `do-while` loops, and the labeled variants extend that control to nested structures.
If you have ever needed to bail out of a deep nested loop, or skip a single iteration without flattening your entire control flow, this is the mechanism that does it.
How `break` and `continue` behave in plain loops
Inside a `for` or `while` loop, `break` terminates the loop entirely when a condition is true, while `continue` jumps straight to the next iteration. Consider a `while` loop incrementing a counter that prints `1`, `2`, `3`, then exits when the counter hits `3` via `break`. The `continue` example works similarly: a loop iterating from 1 to 5 prints one line for every value, but when the counter is between 2 and 4, `continue` skips the second print line. Result: values 2, 3, and 4 get only the "Always printed" message, while 1 and 5 get both.
This is the standard control-flow toolkit for non-trivial loop logic.
When you need labels
Labels come in when nested loops get in the way. Kotlin lets you attach a label to any loop by writing `labelName@` directly before the loop declaration. You can then reference that label inside `break` or `continue` to act on a specific loop, not just the closest one.
Consider an `outerLoop@` and `innerLoop@` pair. When `i` equals `2`, `break@outerLoop` ends the outer loop entirely, even though the `break` is written inside the inner one. Without the label, only the inner loop would exit and the outer loop would keep going.
The same idea applies to `continue`. A `here@` label on an outer `for` loop lets `continue@here` skip the rest of the outer iteration when `i == 3 && j == 3`, jumping back to the next outer pass instead of running the remaining inner iterations.
Why it matters
For most code, the unlabeled versions are enough. They keep control flow obvious and diffs readable. Labeled jumps are powerful, but they are also a code smell: if you need a labeled `continue`, the underlying loop nesting is usually the real problem.
The trade-off is real. Nested loops over small grids or matrices are common in image processing, board games, and calendar grids, and a labeled `break` is often cleaner than threading a boolean flag through three loops. Labeled `continue` is harder to justify—skipping an outer iteration from deep within typically signals a logic design problem.
When to use what
| Situation | Recommendation |
|---|---|
| Single loop, exit early | `break` — straightforward |
| Single loop, skip current pass | `continue` — straightforward |
| Nested loops, exit outer from inner | `break@label` — acceptable |
| Nested loops, skip outer from inner | Refactor instead — `continue@label` is a red flag |
| Looking for first match in collection | `find()` or `firstOrNull()` — cleaner than any labeled loop |
Worked example: bailing out of a grid search
Say you need to scan an 8×8 board for the first empty square. You could write:
“`
Board@ for (row in 0 until 8) {
For (col in 0 until 8) {
If (board[row][col] == EMPTY) {
Selected = Pair(row, col)
Break@board
}
}
}
“`
Without the `board@` label, `break` would only exit the inner column loop and the row loop would keep running, leaving `selected` set to the last empty square on the first row instead of the first overall. With the label, the entire scan terminates cleanly the moment a match is found.
What to watch next
When you reach for a labeled `continue`, rewrite the loop instead. Extract a helper function or use `any`, `find`, or `firstOrNull` from the standard library—they express the same intent without manual loop juggling. Keep labeled `break` in your toolkit for the cases where it genuinely simplifies the code, such as early exit from nested iteration or skipping work for invalid inputs. They are not everyday tools, but when you need them, they save you from contorted flag-passing.
Key takeaways
- `break` and `continue` are your first-line tools for plain loop control in Kotlin.
- Labels (`labelName@`) extend `break` and `continue` to specific nested loops.
- Prefer labeled `break` over labeled `continue`; refactor when only `continue` solves the problem.
- Standard library functions like `find` and `firstOrNull` often replace labeled loops entirely.
Frequently asked questions
What is the difference between `break` and `continue` in Kotlin?
`break` exits the loop entirely and runs the next statement after it. `continue` skips the rest of the current iteration and starts the next pass of the same loop.
When should I use a labeled `break` or `continue`?
Use a labeled `break` when you need to exit a specific outer loop from inside a nested loop. Use labeled `continue` rarely—ideally after considering a refactor.
How do I write a Kotlin label?
Place an identifier followed by `@` directly before the loop, like `outerLoop@ for (i in 1..10)`. Then reference it as `break@outerLoop` or `continue@outerLoop`.
Does Kotlin have a `goto` statement?
No. Kotlin does not include a general `goto`. Labels are limited to `break` and `continue` and only target loop expressions.