break, continue, and Labels
In Kotlin, break and continue are jump statements that change how a loop runs: break exits the loop immediately, while continue skips the rest of the current iteration and moves on to the next one. On their own they only affect the loop that directly contains them, which becomes a problem once loops are nested — that’s where Kotlin’s labels come in, letting you name a loop and target it explicitly from an inner loop. Together, these three tools give you precise control over iteration without resorting to extra boolean flags or restructured code.
Overview: How break, continue, and Labels Work
break terminates the nearest enclosing loop entirely; execution resumes at the first statement after the loop’s closing brace. continue stops the current iteration and jumps straight to the loop’s next iteration check — for a for loop that means advancing to the next element, and for a while or do-while loop it means re-evaluating the condition. Both statements only make sense inside a real loop construct written with for, while, or do-while; the compiler rejects them anywhere else, including inside the lambda you pass to a higher-order function like forEach (more on that in Common Mistakes).
Under the hood, break and continue have the special Kotlin type Nothing — the type of an expression that never completes normally. That’s the same type throw has, and it lets the compiler use break or continue as one branch of an if expression without breaking type inference, since a branch that never returns doesn’t need to contribute a value to the expression’s type.
By default, break and continue target the innermost loop that contains them. When loops are nested, that’s often not what you want — you may need to exit (or skip an iteration of) an outer loop from deep inside an inner one. Kotlin solves this with labels: you prefix a loop with an identifier followed by @, and then reference that identifier from break@label or continue@label anywhere inside it, no matter how many loops are nested in between. This is more explicit and readable than Java-style boolean “shouldBreak” flags or restructuring code into separate functions just to get an early return.
Syntax
The general form of a labeled loop and its labeled jump statements:
loopLabel@ for (item in collection) {
if (someCondition) break@loopLabel
if (otherCondition) continue@loopLabel
}
| Form | Meaning |
|---|---|
break |
Exits the nearest enclosing loop immediately. |
continue |
Skips to the next iteration of the nearest enclosing loop. |
label@ |
Attaches a name to the loop that immediately follows it. |
break@label |
Exits the loop marked with that label, even from inside nested loops. |
continue@label |
Jumps to the next iteration of the loop marked with that label. |
return@label |
Not a loop jump — returns from a labeled lambda, commonly used to simulate early exit inside forEach and similar functions. |
A label can be any valid identifier; by convention it’s written in lowerCamelCase followed directly by @, with no space, immediately before the loop keyword.
Examples
Example 1: break stops the loop entirely
fun main() {
for (i in 1..10) {
if (i == 5) {
break
}
println(i)
}
println("Done")
}
Output:
1
2
3
4
Done
The loop counts up from 1, but as soon as i reaches 5 the break statement fires and the loop ends immediately — 5 through 10 are never printed, and control moves straight to println("Done") after the loop.
Example 2: continue skips an iteration
fun main() {
for (i in 1..10) {
if (i % 2 == 0) {
continue
}
println(i)
}
}
Output:
1
3
5
7
9
Here the loop still runs all ten iterations, but whenever i is even, continue skips the println(i) call for that iteration and moves straight on to the next value of i. The loop itself never ends early — only individual iterations are skipped.
Example 3: labeled break to exit nested loops
fun main() {
val grid = arrayOf(
intArrayOf(1, 2, 3),
intArrayOf(4, 5, 6),
intArrayOf(7, 8, 9)
)
val target = 5
var found = false
search@ for (row in grid) {
for (value in row) {
if (value == target) {
found = true
break@search
}
}
}
println("Found $target: $found")
}
Output:
Found 5: true
This searches a 2D grid for a target value. found is declared with var because its value changes once a match is located — val would not compile here since it’s reassigned inside the loop. An unlabeled break inside the inner loop would only stop scanning the current row and let the outer loop move on to the next one — wasteful once the value has already been found. Labeling the outer loop search@ lets break@search, called from inside the inner loop, exit both loops in one step as soon as the target is located.
How It Works Step by Step
Walking through Example 3:
- The outer loop binds
rowtointArrayOf(1, 2, 3), the first row. - The inner loop iterates that row’s values: 1, then 2, then 3. None equals
target(5), so the inner loop finishes normally and the outer loop advances. - The outer loop binds
rowtointArrayOf(4, 5, 6). The inner loop checks 4 (no match), then 5 — this matches, sofoundis set totrueandbreak@searchruns. - Because the label targets the outer loop, execution doesn’t just exit the inner
for; it exits both loops immediately and jumps to the first statement after the outer loop’s closing brace — theprintlncall. The third row,intArrayOf(7, 8, 9), is never even visited.
The same mechanics apply to continue@label: instead of abandoning the labeled loop entirely, it abandons only the current iteration of that labeled loop and jumps to its next iteration check, skipping any remaining nested loop work in between.
Common Mistakes
Mistake 1: assuming an unlabeled break exits every loop
It’s easy to write a nested loop expecting break to stop everything, when it actually only stops the innermost loop:
fun main() {
for (i in 1..3) {
for (j in 1..3) {
if (j == 2) {
break
}
println("i=$i, j=$j")
}
}
}
Output:
i=1, j=1
i=2, j=1
i=3, j=1
The break only ends the inner j loop each time j reaches 2 — the outer i loop keeps running for all three values of i, restarting the inner loop each time. If the intent was to stop everything the first time j == 2 happens, label the outer loop:
fun main() {
outer@ for (i in 1..3) {
for (j in 1..3) {
if (j == 2) {
break@outer
}
println("i=$i, j=$j")
}
}
}
Output:
i=1, j=1
Now break@outer exits both loops the very first time the condition is met.
Mistake 2: trying to use break or continue inside forEach
forEach looks like a loop, but it’s actually a regular inline function call that takes a lambda — it isn’t a for, while, or do-while construct, so the compiler flatly rejects break and continue inside its lambda:
fun main() {
listOf(1, 2, 3, 4).forEach {
if (it == 3) break
println(it)
}
}
Output:
Compiler error: 'break' and 'continue' are not allowed here, only within a loop
To skip an item, use a labeled return targeting the implicit lambda label, which shares its name with the function:
fun main() {
listOf(1, 2, 3, 4).forEach {
if (it == 3) return@forEach
println(it)
}
}
Output:
1
2
4
return@forEach only returns from the current lambda invocation, so it behaves like continue, not break — the loop still visits every remaining element (3 is skipped but 4 is still printed). There’s no direct equivalent that truly stops forEach early; if you need real break behavior, use a plain for loop, or reach for a purpose-built function like takeWhile, firstOrNull, or indexOfFirst.
Mistake 3: referencing a label that was never declared
fun main() {
for (i in 1..3) {
if (i == 2) break@outer
println(i)
}
}
Output:
Compiler error: target label 'outer' does not exist
The loop was never labeled outer@, so break@outer has nothing to bind to. The label name in the jump statement must exactly match a label written directly before an enclosing loop.
Best Practices
- Reach for a label only when you actually need to affect an outer loop from an inner one — for a single loop, plain
break/continueis clearer. - Give labels descriptive names (
search@,rows@) rather than single letters, since they’re read far from where they’re declared. - Remember that
break/continuecannot cross into or out of a lambda passed to functions likeforEach,map, orfilter— usereturn@label, or switch to a realfor/whileloop when you need a genuine early exit. - Prefer expressive standard-library functions (
any,none,first,indexOfFirst,takeWhile) over manual loops with flags andbreakwhen the intent is a simple search or filter — they read as clearly as English and avoid mutable state. - In a
do-whileloop, remembercontinuejumps to the condition check at the bottom, not back to the top of the body — the condition may reference variables set earlier in the same iteration. - Avoid deeply nested labeled loops as a substitute for extracting a small helper function; a function with a normal
returnis often more readable than three levels of labels.
Practice Exercises
- Loop over the numbers 1 through 20. Use
continueto skip any multiple of 3, and usebreakto stop the loop entirely once a number greater than 15 is reached. Print each number that isn’t skipped. - Given a list of rows, where each row is a list of strings, use a labeled
forloop andbreak@labelto find the first row and column index where a target string appears, then print those indices. - Take a list of integers and try to stop at the first negative number using
forEachwithbreak— confirm it fails to compile, then rewrite the same logic two ways: once usingreturn@forEach(note how its behavior differs from a real break), and once using a plainforloop with an unlabeledbreakthat truly stops early.
Summary
breakexits the nearest enclosing loop immediately;continueskips to that loop’s next iteration.- Both statements only work inside real
for,while, ordo-whileloops — never inside a lambda like the one passed toforEach. breakandcontinuehave the typeNothing, meaning they never complete normally, which is why the compiler can use them inside expression branches.- A label (
name@) placed before a loop letsbreak@nameorcontinue@nametarget that specific loop from anywhere inside it, even through nested loops. - Inside
forEachand similar higher-order functions, usereturn@functionNameto skip the current element — it behaves likecontinue, notbreak, since it can’t stop the function from visiting the rest of the collection. - Referencing an undeclared label, or expecting an unlabeled
breakto exit more than the innermost loop, are the two most common mistakes with this feature.
