Skip to content

`for` Loops

A while loop repeats while a condition is true.

for Loops

A while loop repeats while a condition is true.

A for loop is different. It walks through items.

Use for when you already have a group of values and want to visit each one.

const std = @import("std");

pub fn main() void {
    const numbers = [_]u8{ 10, 20, 30 };

    for (numbers) |n| {
        std.debug.print("{}\n", .{n});
    }
}

This prints:

10
20
30

The array is:

const numbers = [_]u8{ 10, 20, 30 };

The loop is:

for (numbers) |n| {
    std.debug.print("{}\n", .{n});
}

Read it as:

for each item in numbers, call the current item n, then run the loop body.

The Basic Shape

The basic shape is:

for (items) |item| {
    // use item
}

The name between | and | is the loop capture.

|item|

That name refers to the current item.

You can choose a better name:

for (users) |user| {
    // use user
}

for (bytes) |byte| {
    // use byte
}

for (tokens) |token| {
    // use token
}

Good names make loops easier to read.

Looping Over an Array

An array has a fixed length known at compile time.

const letters = [_]u8{ 'a', 'b', 'c' };

for (letters) |ch| {
    std.debug.print("{c}\n", .{ch});
}

This prints:

a
b
c

The format string uses {c} because each item is a character.

Looping Over a Slice

A slice is a view into a sequence of items.

const values = [_]u8{ 5, 6, 7, 8 };
const slice = values[1..3];

for (slice) |value| {
    std.debug.print("{}\n", .{value});
}

This prints:

6
7

The slice:

values[1..3]

starts at index 1 and stops before index 3.

So it contains:

6, 7

for works naturally with slices. This is one of the most common loop patterns in Zig.

Looping Over a String

A string literal in Zig is a sequence of bytes.

const text = "abc";

for (text) |byte| {
    std.debug.print("{c}\n", .{byte});
}

This prints:

a
b
c

This loops over bytes, not Unicode characters.

For plain ASCII text, each byte is one visible character. For UTF-8 text, one visible character may use more than one byte. We will study UTF-8 later. For now, remember this rule: for over a string gives bytes.

Getting the Index

Often you need both the item and its index.

Use a range beside the collection:

const std = @import("std");

pub fn main() void {
    const names = [_][]const u8{ "ada", "zig", "c" };

    for (names, 0..) |name, index| {
        std.debug.print("{}: {s}\n", .{ index, name });
    }
}

This prints:

0: ada
1: zig
2: c

The loop is:

for (names, 0..) |name, index| {

This means:

walk through names

also count upward starting from 0

call the current item name

call the current index index

The order matters. The captures match the inputs.

for (names, 0..) |name, index| {

name comes from names.

index comes from 0...

Ranges

A range can be used by itself.

for (0..5) |i| {
    std.debug.print("{}\n", .{i});
}

This prints:

0
1
2
3
4

The range:

0..5

starts at 0 and stops before 5.

So it includes 0, 1, 2, 3, and 4.

The end is exclusive.

Start Is Included, End Is Excluded

This is important:

0..5

means:

0, 1, 2, 3, 4

It does not include 5.

This same rule appears in slice syntax:

items[0..5]

That means items at indexes 0, 1, 2, 3, and 4.

The rule is consistent: start included, end excluded.

Modifying Items

By default, a for loop gives you each item as a value.

If you want to modify items, loop over pointers.

const std = @import("std");

pub fn main() void {
    var numbers = [_]u8{ 1, 2, 3 };

    for (&numbers) |*n| {
        n.* += 10;
    }

    for (numbers) |n| {
        std.debug.print("{}\n", .{n});
    }
}

This prints:

11
12
13

The important line is:

for (&numbers) |*n| {

&numbers gives access to the array by reference.

|*n| captures a pointer to each item.

Inside the loop, n is a pointer. To read or write the pointed-to value, use:

n.*

So this:

n.* += 10;

means:

add 10 to the actual item inside the array.

Why |*n| Matters

This loop does not change the array:

for (numbers) |n| {
    var copy = n;
    copy += 10;
}

Here, n is a copy of each item. Changing a copy does not change the array.

This loop changes the array:

for (&numbers) |*n| {
    n.* += 10;
}

Here, n points to the actual item.

Use pointer capture only when you need mutation.

continue in a for Loop

continue skips the rest of the current loop body.

const std = @import("std");

pub fn main() void {
    const numbers = [_]u8{ 1, 2, 3, 4, 5 };

    for (numbers) |n| {
        if (n % 2 == 0) {
            continue;
        }

        std.debug.print("{}\n", .{n});
    }
}

This prints:

1
3
5

When n is even, the loop skips the print statement.

break in a for Loop

break exits the loop immediately.

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
20

When n becomes 30, the loop stops.

for with else

A for loop can have an else branch.

The else branch runs if the loop finishes normally.

const std = @import("std");

pub fn main() void {
    const numbers = [_]u8{ 1, 2, 3 };

    for (numbers) |n| {
        std.debug.print("{}\n", .{n});
    } else {
        std.debug.print("done\n", .{});
    }
}

This prints:

1
2
3
done

If the loop exits with break, the else branch does not run.

const std = @import("std");

pub fn main() void {
    const numbers = [_]u8{ 1, 2, 3 };

    for (numbers) |n| {
        if (n == 2) {
            break;
        }

        std.debug.print("{}\n", .{n});
    } else {
        std.debug.print("done\n", .{});
    }
}

This prints:

1

The loop stopped early, so else did not run.

This is useful for search logic.

Searching with for

Suppose you want to know whether an array contains a value.

const std = @import("std");

pub fn main() void {
    const numbers = [_]u8{ 3, 6, 9, 12 };
    const target: u8 = 9;

    const found = for (numbers) |n| {
        if (n == target) {
            break true;
        }
    } else false;

    std.debug.print("found = {}\n", .{found});
}

This prints:

found = true

Read it like this:

loop through the numbers

if we find the target, stop and return true

if the loop finishes without finding it, return false

This is compact, but still explicit.

Looping Over Two Collections

A for loop can walk through multiple collections at the same time.

const std = @import("std");

pub fn main() void {
    const names = [_][]const u8{ "Ada", "Bob", "Cora" };
    const scores = [_]u8{ 90, 75, 88 };

    for (names, scores) |name, score| {
        std.debug.print("{s}: {}\n", .{ name, score });
    }
}

This prints:

Ada: 90
Bob: 75
Cora: 88

The first capture comes from the first collection.

The second capture comes from the second collection.

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

name comes from names.

score comes from scores.

The collections must have compatible lengths for the loop.

Ignoring a Value

Sometimes you do not need one of the captured values.

Use _ to ignore it.

const values = [_]u8{ 10, 20, 30 };

for (values, 0..) |_, index| {
    std.debug.print("index = {}\n", .{index});
}

This prints:

index = 0
index = 1
index = 2

The item itself is ignored. Only the index is used.

for vs while

Use for when you are walking through known items.

for (items) |item| {
    // use item
}

Use while when repetition depends on a condition.

while (remaining > 0) {
    // keep working
}

Use for for arrays, slices, strings, and ranges.

Use while for open-ended loops, retry loops, and loops that stop based on changing state.

The Main Idea

A for loop walks through items.

It is the normal Zig tool for arrays, slices, strings, and ranges.

The most important forms are:

for (items) |item| {
    // use item
}
for (items, 0..) |item, index| {
    // use item and index
}
for (&items) |*item| {
    // modify item
}

Keep this rule in mind: use for when you already have things to visit. Use while when you only have a condition.