The loops in Zig are structures for repeating code. If you come from C, Java, or JavaScript, you’re probably used to for being a multi-purpose tool; in Zig, that responsibility is more distributed.
In Zig, things are more compartmentalized to improve clarity:
while: Used when repetition depends on a boolean condition or we want a C-style loop.for: Used for iterating over sequences (arrays, slices, tuples, or vectors) and number ranges.
The while loop
The most basic form of while is identical to most languages. It repeats as long as the condition is true.
var i: u8 = 0;
while (i < 5) {
std.debug.print("Counting: {d}\n", .{i});
i += 1;
}The continuation expression (:)
Here is where Zig improves upon the classic design. In a complex while loop, we often need to increment a counter or advance an iterator.
If we use the continue statement inside the loop to jump to the next iteration, we run the risk of also skipping the line that increments the counter, creating an infinite loop.
Zig solves this by allowing the update expression to be defined as part of the loop header, separated by a colon :.
var i: u8 = 0;
// while (condition) : (action_at_end_of_loop)
while (i < 10) : (i += 1) {
if (i == 5) continue; // Skips, but EXECUTES i += 1
std.debug.print("{d} ", .{i});
}This is equivalent to C’s for (int i = 0; i < 10; i++), but retains the “while” semantics.
The for loop
In Zig, the for loop is fundamentally a “for-each” loop. It is not used for arbitrary conditions, but to iterate over things with a known start and end.
Iterating over arrays and slices
The syntax uses vertical bars | | to “capture” the current element of the iteration.
const primes = [_]u8{ 2, 3, 5, 7, 11 };
for (primes) |num| {
std.debug.print("Prime: {d}\n", .{num});
}Important: The captured variable (num) is a constant containing a copy of the value. You cannot do num += 1 inside the loop to modify the original array.
Modifying the original array (|*ptr|)
If we need to modify the array elements while iterating, we must capture a pointer to the element, using an asterisk.
var numbers = [_]i32{ 1, 2, 3 };
for (&numbers) |*n| {
n.* += 10; // Access the value through the pointer and add
}
// Now numbers is { 11, 12, 13 }Getting the index
Sometimes we need to know our position. Zig allows capturing the index by adding a second variable in the capture. For this to work, we often specify the range 0...
const vowels = "aeiou";
// Iterate over vowels and simultaneously generate indices starting from 0
for (vowels, 0..) |letter, i| {
std.debug.print("Vowel {d} is {c}\n", .{ i, letter });
}Iterating number ranges
To iterate “from 0 to 10”, Zig offers the range syntax start..end.
// Iterates from 0 to 9 (the upper bound is exclusive)
for (0..10) |i| {
std.debug.print("Iteration {d}\n", .{i});
}Note that 0..10 excludes 10. If you want to iterate N times, you simply write 0..N.
Iterating multiple sequences at once
A for loop can iterate over multiple sequences at once, as long as they have the same length.
const names = [_][]const u8{ "Ana", "Beto", "Carla" };
const ages = [_]u8{ 25, 30, 22 };
// Advances in both arrays simultaneously
for (names, ages) |n, a| {
std.debug.print("{s} is {d} years old\n", .{ n, a });
}This eliminates the need to manage indices manually (names[i], ages[i]) and makes the code much safer (the compiler or runtime will verify the lengths match).
break and else in loops
Both while and for support break to exit prematurely. But Zig adds an interesting twist: loops can have an else block.
The code inside else runs only if the loop ended “naturally” (i.e., the condition became false or the range ended), but NOT if it was exited using break.
// Searching for a number
for (numbers) |n| {
if (n == target) {
std.debug.print("Found!\n", .{});
break;
}
} else {
// Only executed if the for loop ended without finding anything
std.debug.print("Target not found\n", .{});
}This structure is useful in search algorithms and avoids needing a boolean found variable outside the loop.