A label in Zig is a name we give to a loop or block to control where we exit or continue from. It is useful when you have a loop inside another and, upon finding what you’re looking for, you want to exit everything.
In languages like C or C++, the keyword break only breaks the innermost loop. To exit both, we often resort to “dirty” tricks such as:
- Creating a boolean variable
found = trueand checking it in the outer loop. - Using a
goto😱. - Refactoring the code into a separate function and using
return.
Zig offers an explicit and readable solution: labels.
Syntax of labels
A label in Zig is simply an identifier followed by a colon :, placed right before a loop statement (while, for) or a block.
label_name: while (condition) {
...
}Once the loop is labeled, we can use break :label_name or continue :label_name to refer specifically to that loop, no matter how deeply nested we are.
Breaking nested loops
Imagine we are searching for a specific value in a 10x10 matrix.
const matrix = [3][3]u8{
.{ 1, 2, 3 },
.{ 4, 5, 6 },
.{ 7, 8, 9 },
};
pub fn main() void {
const target = 5;
// Label the outer loop
outer_loop: for (matrix, 0..) |row, i| {
for (row, 0..) |value, j| {
if (value == target) {
std.debug.print("Found at [{d}, {d}]\n", .{i, j});
// Break directly from the main loop
break :outer_loop;
}
}
}
std.debug.print("Search finished.\n", .{});
}If we had used a normal break (without a label), we would only have exited the inner for, and the outer loop would have continued searching the next row unnecessarily.
This syntax maintains the “Zen of Zig”: the flow is explicit. When reading break :outer_loop, you know exactly where execution will jump, without having to trace closing braces.
Labels in continue
The same applies to continue. If we are in an inner loop and want to force the next iteration of the outer loop, we use the label.
processing: for (items) |item| {
while (complex_condition) {
// ... logic ...
if (item_is_invalid) {
// Stop processing this item and move to the next one in the for loop
continue :processing;
}
}
}Labels in blocks
Labels are not exclusive to loops. We can also label a block of code { ... }.
This allows initializing constants whose logic requires several steps.
If you want to compute a const constant, but the logic requires temporary variables or several conditions, in other languages you might need an auxiliary function or an immediately invoked expression.
In Zig, we use a labeled block:
const complex_number = calculation_block: {
var x: i32 = 10;
x += 5;
if (x > 20) {
// "Break" out of the block by returning a value
break :calculation_block 100;
}
// Default value of the block
break :calculation_block x * 2;
};The important syntax is break :label value;.
This works just like a return in a function, but for a local block.
- The code inside the block executes.
- The variables
xdie upon exiting the block (they don’t pollute the scope). - The result is assigned to
complex_numberimmutably (const).
This technique is the idiomatic way in Zig to emulate complex expressions that in Rust are done simply by returning the last value of the block.