Skip to content

`while` Loops

Programs often need to repeat work.

while Loops

Programs often need to repeat work.

A program may need to:

read lines from a file

walk through items in a buffer

retry an operation

count from one number to another

keep running until the user quits

In Zig, one basic way to repeat work is with while.

const std = @import("std");

pub fn main() void {
    var i: u8 = 0;

    while (i < 5) {
        std.debug.print("i = {}\n", .{i});
        i += 1;
    }
}

This prints:

i = 0
i = 1
i = 2
i = 3
i = 4

The loop starts with i = 0.

Before each round, Zig checks:

i < 5

If the condition is true, the loop body runs.

After the body runs, i += 1 increases i.

When i becomes 5, the condition becomes false, and the loop stops.

The Basic Shape

The basic shape of while is:

while (condition) {
    // repeated code
}

The condition must be a boolean.

This is valid:

while (i < 10) {
    i += 1;
}

This is invalid:

while (i) {
    // invalid
}

Zig does not treat integers as booleans. Write the condition explicitly:

while (i != 0) {
    i -= 1;
}

This strictness is useful. It prevents unclear code.

A Loop Needs Progress

A while loop must eventually make progress, unless you intentionally want an infinite loop.

This loop stops:

var i: u8 = 0;

while (i < 3) {
    std.debug.print("{}\n", .{i});
    i += 1;
}

This loop never stops:

var i: u8 = 0;

while (i < 3) {
    std.debug.print("{}\n", .{i});
}

The value of i never changes, so i < 3 is always true.

This is one of the most common beginner mistakes with loops: forgetting to update the variable that controls the loop.

The Continue Expression

Zig has a useful form of while with a continue expression.

var i: u8 = 0;

while (i < 5) : (i += 1) {
    std.debug.print("i = {}\n", .{i});
}

This prints:

i = 0
i = 1
i = 2
i = 3
i = 4

The part after the colon is:

: (i += 1)

This runs after each loop body.

So this:

while (i < 5) : (i += 1) {
    std.debug.print("i = {}\n", .{i});
}

means:

check i < 5

run the body

run i += 1

repeat

This keeps the loop update close to the loop condition.

continue

The continue statement skips the rest of the current loop body and moves to the next round.

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
5

When i is 3, the loop reaches:

continue;

That skips the print statement.

Because this loop has a continue expression, i += 1 still runs before the next condition check.

break

The break statement exits the loop immediately.

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
3

When i becomes 4, the loop stops.

break is useful when the loop condition alone does not describe every reason to stop.

Infinite Loops

Sometimes a program should run until something inside the loop stops it.

Use while (true) for this.

var count: u8 = 0;

while (true) {
    if (count == 3) {
        break;
    }

    std.debug.print("count = {}\n", .{count});
    count += 1;
}

This prints:

count = 0
count = 1
count = 2

The condition true never becomes false. The loop stops only because of break.

This pattern is common in servers, command loops, interpreters, and programs that read input until the input ends.

while Can Return a Value

Like if and switch, while can be an expression.

A while expression may have an else branch.

The else branch runs when the loop condition becomes false naturally.

const std = @import("std");

pub fn main() void {
    var i: u8 = 0;

    const result = while (i < 3) : (i += 1) {
        std.debug.print("i = {}\n", .{i});
    } else "finished";

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

This prints:

i = 0
i = 1
i = 2
finished

Here, the loop ends because i < 3 becomes false. Then the else branch produces "finished".

For beginners, this form is less common than a normal loop. But it shows an important Zig idea: control flow constructs can be expressions.

while with break Value

A loop can produce a value through break.

const std = @import("std");

pub fn main() void {
    var i: u8 = 0;

    const found = while (i < 10) : (i += 1) {
        if (i == 4) {
            break true;
        }
    } else false;

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

This prints:

found = true

Read it this way:

search while i < 10

if i == 4, stop the loop and return true

if the loop finishes normally, return false

This is useful for search logic.

while with Optionals

Zig can use while to unwrap optional values.

An optional value either contains a value or is null.

const maybe_number: ?u32 = 10;

The type ?u32 means: either a u32, or null.

Here is a simple optional loop:

const std = @import("std");

fn nextNumber(i: *u8) ?u8 {
    if (i.* >= 3) return null;

    const value = i.*;
    i.* += 1;
    return value;
}

pub fn main() void {
    var i: u8 = 0;

    while (nextNumber(&i)) |n| {
        std.debug.print("n = {}\n", .{n});
    }
}

This prints:

n = 0
n = 1
n = 2

The line:

while (nextNumber(&i)) |n| {

means:

call nextNumber(&i)

if it returns a value, name that value n

run the loop body

if it returns null, stop the loop

This pattern is useful when reading items one at a time until there are no more items.

while with Error Unions

while can also work with error unions.

Suppose a function returns either a value or an error:

fn readByte() !u8 {
    // may return a byte or an error
}

You can handle successful values and errors separately.

A simple example:

const std = @import("std");

const ReadError = error{
    End,
};

fn next(i: *u8) ReadError!u8 {
    if (i.* >= 3) return error.End;

    const value = i.*;
    i.* += 1;
    return value;
}

pub fn main() void {
    var i: u8 = 0;

    while (next(&i)) |value| {
        std.debug.print("value = {}\n", .{value});
    } else |err| {
        std.debug.print("stopped with error: {}\n", .{err});
    }
}

This prints:

value = 0
value = 1
value = 2
stopped with error: error.End

The loop continues while next(&i) returns a successful value.

When next(&i) returns an error, the loop stops and the else branch receives the error.

while vs for

Use while when the loop depends on a condition.

while (bytes_read < total) {
    // keep reading
}

Use while when you do not know exactly how many times the loop will run.

while (try readLine()) |line| {
    // process line
}

Use for when you already have a collection and want to visit each item.

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

We will cover for in the next section.

The Main Idea

while repeats code while a condition is true.

The condition must be a boolean. The loop should make progress. Use continue to skip to the next round. Use break to leave the loop early.

Zig also lets while work with optionals, error unions, and returned values. For now, focus on the simple form:

while (condition) {
    // repeat this
}

That one form is enough to write many useful programs.