⚡ Zig Guide LiveUnofficialbut fully verified
✓ Zig 0.17.0-dev.2122+3e15e99e6What's newOn an older Zig?

For Loops

const std = @import("std");
const expect = std.testing.expect;

test "iterate values" {
    const string = [_]u8{ 'a', 'b', 'c' };
    var count: usize = 0;
    for (string) |character| {
        if (character == 'b') count += 1;
    }
    try expect(count == 1);
}

test "iterate with an index" {
    const string = [_]u8{ 'a', 'b', 'c' };
    var last_index: usize = 0;
    for (string, 0..) |_, index| {
        last_index = index;
    }
    try expect(last_index == 2);
}

test "ranges" {
    var sum: usize = 0;
    for (0..5) |i| sum += i;
    try expect(sum == 10);
}

test "iterate two sequences at once" {
    // Multi-object `for` requires equal lengths; a mismatch is checked.
    const names = [_][]const u8{ "a", "b" };
    const scores = [_]u8{ 1, 2 };
    var total: u8 = 0;
    for (names, scores) |name, score| {
        _ = name;
        total += score;
    }
    try expect(total == 3);
}

test "mutate through a pointer capture" {
    var numbers = [_]u8{ 1, 2, 3 };
    for (&numbers) |*n| n.* *= 2;
    try expect(numbers[2] == 6);
}

Zig’s for iterates over something: a slice, an array, a range. There is no three-clause for (i = 0; i < n; i++); that is what while is for. The result is that the common case carries no opportunity for an off-by-one. You do not write the bound, so you cannot write it wrong.

Indices are opt-in

for (items, 0..) |item, index| { ... }

The 0.. is a second sequence to iterate in lockstep, not special syntax. You only pay for the index when you ask for it, and the loop still cannot run off the end, because items is what decides how long it runs.

Iterating several sequences together

The same mechanism zips any number of sequences:

for (names, scores) |name, score| { ... }

Lengths must match. When both are known at compile time, a mismatch is a compile error that names the lengths:

error: non-matching for loop lengths
note: length 3 here
note: length 2 here

When they are runtime slices, it is a safety check that panics in Debug and ReleaseSafe. Either way this cannot silently read past the end of the shorter one, which is the bug the equivalent indexed loop invites.

A range with no upper bound pairs with anything: 0.. takes its length from whatever it is zipped with. A bare for (0..) with nothing to bound it is a compile error, because nothing says when to stop.

Mutating in place

Capturing by value gives you a copy. To modify the underlying elements, iterate over a pointer to the array and capture by pointer:

for (&numbers) |*n| n.* *= 2;

Both halves are needed. &numbers is what makes the loop walk the original rather than a copy of the array, and |*n| is what makes each capture a pointer into it. Leave off either and the loop compiles and does nothing useful.

For a slice, the & is unnecessary: a slice already refers to memory it does not own, so for (slice) |*item| writes through to the underlying elements.

Going backwards

There is no reverse range and no step. Counting down is a while:

var i = items.len;
while (i > 0) {
    i -= 1;
    use(items[i]);
}

The decrement comes first for a reason: items.len is a usize, and a loop written while (i >= 0) : (i -= 1) never terminates, because an unsigned counter cannot go below zero. In a safety build it panics on the wraparound instead, which at least tells you. That asymmetry is a deliberate cost. The forward loop is the one that is safe by construction, and going backwards means opting back into managing the index yourself.

for also breaks and yields

break, continue, labels and else all work here exactly as on while. A for used as an expression yields a value with break x and needs an else for the ran-out-of-elements case, which makes linear search a single expression. See loops as expressions and labelled loops.

inline for unrolls the loop at compile time and makes the capture comptime-known. That is what lets a loop walk the fields of a struct or the tags of an enum. That is a different tool with a different purpose, covered in inline loops.