Loops repeat code. But sometimes you do not want a loop to finish in the normal way.
break and continue
Loops repeat code. But sometimes you do not want a loop to finish in the normal way.
You may want to stop early.
You may want to skip one item.
You may want to leave an outer loop from inside an inner loop.
Zig gives you two simple tools for this:
breakand:
continuebreak exits a loop.
continue skips to the next loop iteration.
break Stops a Loop
Here is a simple loop:
const std = @import("std");
pub fn main() void {
var i: u8 = 0;
while (i < 10) : (i += 1) {
if (i == 4) {
break;
}
std.debug.print("{}\n", .{i});
}
}This prints:
0
1
2
3The loop would normally continue until i reaches 10.
But this branch stops it early:
if (i == 4) {
break;
}When i becomes 4, break runs. The loop ends immediately.
The print statement does not run for 4.
break in a for Loop
break works in for loops too.
const std = @import("std");
pub fn main() void {
const numbers = [_]u8{ 10, 20, 30, 40 };
for (numbers) |n| {
if (n == 30) {
break;
}
std.debug.print("{}\n", .{n});
}
}This prints:
10
20The loop stops when it reaches 30.
This is useful for search logic. Once you find what you want, there is no reason to keep looping.
continue Skips One Iteration
continue does not stop the whole loop.
It skips the rest of the current iteration and moves to the next one.
const std = @import("std");
pub fn main() void {
var i: u8 = 0;
while (i < 6) : (i += 1) {
if (i == 3) {
continue;
}
std.debug.print("{}\n", .{i});
}
}This prints:
0
1
2
4
5When i is 3, this line runs:
continue;The rest of the loop body is skipped.
So this line does not run for 3:
std.debug.print("{}\n", .{i});Then the loop continues with the next value.
continue in a while Loop with a Continue Expression
Remember this form:
while (condition) : (update) {
// body
}The update part still runs after continue.
Example:
const std = @import("std");
pub fn main() void {
var i: u8 = 0;
while (i < 5) : (i += 1) {
if (i == 2) {
continue;
}
std.debug.print("{}\n", .{i});
}
}This prints:
0
1
3
4When i == 2, continue runs. Zig skips the print, then runs:
i += 1So the loop still moves forward.
This is important. Without the continue expression, you could accidentally create an infinite loop.
A Common Mistake
This loop never ends:
var i: u8 = 0;
while (i < 5) {
if (i == 2) {
continue;
}
i += 1;
}When i becomes 2, the loop reaches continue.
That skips:
i += 1;So i stays 2 forever.
A safer version is:
var i: u8 = 0;
while (i < 5) : (i += 1) {
if (i == 2) {
continue;
}
}Now the update happens even when continue is used.
Skipping Items with continue
continue is useful when some items should be ignored.
const std = @import("std");
pub fn main() void {
const numbers = [_]u8{ 1, 2, 3, 4, 5, 6 };
for (numbers) |n| {
if (n % 2 == 0) {
continue;
}
std.debug.print("{}\n", .{n});
}
}This prints:
1
3
5The expression:
n % 2 == 0checks whether n is even.
If it is even, the loop skips it.
So only odd numbers are printed.
Searching with break
break is useful when searching.
const std = @import("std");
pub fn main() void {
const numbers = [_]u8{ 4, 8, 15, 16, 23, 42 };
const target: u8 = 16;
var found = false;
for (numbers) |n| {
if (n == target) {
found = true;
break;
}
}
std.debug.print("found = {}\n", .{found});
}This prints:
found = trueThe loop stops as soon as it finds the target.
That is better than checking the remaining items for no reason.
break Can Return a Value
In Zig, loops can be expressions.
That means break can return a value from a loop.
const std = @import("std");
pub fn main() void {
const numbers = [_]u8{ 4, 8, 15, 16, 23, 42 };
const target: u8 = 16;
const found = for (numbers) |n| {
if (n == target) {
break true;
}
} else false;
std.debug.print("found = {}\n", .{found});
}This prints:
found = trueRead this part carefully:
const found = for (numbers) |n| {
if (n == target) {
break true;
}
} else false;It means:
loop through the numbers
if target is found, stop and make the loop value true
if the loop finishes normally, use the else value false
So found becomes either true or false.
This is a clean Zig pattern for search code.
break Value Must Match the else Value
When a loop returns a value, the break value and the else value must have compatible types.
This is valid:
const result = for (items) |item| {
if (item == target) {
break true;
}
} else false;Both values are booleans.
This is invalid:
const result = for (items) |item| {
if (item == target) {
break true;
}
} else "not found";One value is a boolean. The other is a string.
Zig rejects this because result needs one clear type.
break from a Labeled Block
break can also leave a labeled block.
const std = @import("std");
pub fn main() void {
const value = blk: {
if (true) {
break :blk 123;
}
break :blk 0;
};
std.debug.print("value = {}\n", .{value});
}This prints:
value = 123The label is:
blk:The line:
break :blk 123;means:
leave the block named blk and make its value 123.
This is not a loop. It is a labeled block expression.
break from an Outer Loop
Without labels, break exits the nearest loop.
for (0..3) |i| {
for (0..3) |j| {
if (j == 1) {
break;
}
std.debug.print("i={}, j={}\n", .{ i, j });
}
}This prints:
i=0, j=0
i=1, j=0
i=2, j=0The break exits only the inner loop.
To exit the outer loop, use a label:
const std = @import("std");
pub fn main() void {
outer: for (0..3) |i| {
for (0..3) |j| {
if (i == 1 and j == 1) {
break :outer;
}
std.debug.print("i={}, j={}\n", .{ i, j });
}
}
}This prints:
i=0, j=0
i=0, j=1
i=0, j=2
i=1, j=0The label is:
outer:The line:
break :outer;means:
leave the loop named outer.
continue to an Outer Loop
A labeled continue moves to the next iteration of a named loop.
const std = @import("std");
pub fn main() void {
outer: for (0..3) |i| {
for (0..3) |j| {
if (j == 1) {
continue :outer;
}
std.debug.print("i={}, j={}\n", .{ i, j });
}
}
}This prints:
i=0, j=0
i=1, j=0
i=2, j=0When j == 1, this runs:
continue :outer;That skips the rest of the inner loop and starts the next iteration of the outer loop.
Use labeled continue only when it makes the code clearer. In many cases, a helper function is easier to read.
Early Exit for Validation
break and continue are useful in validation code.
Suppose we want to check that all bytes are lowercase ASCII letters.
const std = @import("std");
fn isLowercaseAscii(text: []const u8) bool {
for (text) |byte| {
if (byte < 'a' or byte > 'z') {
return false;
}
}
return true;
}
pub fn main() void {
const ok = isLowercaseAscii("zig");
std.debug.print("ok = {}\n", .{ok});
}This prints:
ok = trueThis example uses return, not break, because the loop is inside a function and the whole function can stop immediately.
But the idea is the same: once you know the answer, exit early.
Prefer Clear Exits
Early exits are useful, but too many can make code hard to follow.
This is usually clear:
for (items) |item| {
if (!item.valid) {
continue;
}
process(item);
}The continue removes an unwanted case early.
This is also clear:
for (items) |item| {
if (item.id == target_id) {
break;
}
}The break stops after finding the target.
But deeply nested code with many labels can become difficult:
outer: for (a) |x| {
middle: for (b) |y| {
inner: for (c) |z| {
if (some_condition) {
break :outer;
}
}
}
}Sometimes this is correct. But often, a small helper function is better.
The Main Idea
break exits a loop.
continue skips the current iteration and moves to the next one.
Use break when the loop has done enough work.
Use continue when the current item should be ignored.
Use labels when you need to control an outer loop.
The beginner rule is simple: keep loop exits obvious. A reader should be able to see quickly why the loop stops or why an item is skipped.