⚡ Zig Guide LiveUnofficial
✓ Zig 0.17.0-dev.1454+5faa79730On an older Zig?

Labelled Loops

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

test "break out of nested loops" {
    var found: ?[2]usize = null;
    outer: for (0..5) |i| {
        for (0..5) |j| {
            if (i * j == 6) {
                found = .{ i, j };
                break :outer; // leaves both loops
            }
        }
    }
    try expect(found.?[0] == 2);
    try expect(found.?[1] == 3);
}

test "continue an outer loop" {
    var count: u32 = 0;
    outer: for (0..4) |i| {
        for (0..4) |j| {
            if (j > i) continue :outer; // next i, skip remaining j
            count += 1;
        }
    }
    try expect(count == 10); // 1 + 2 + 3 + 4
}

test "labelled loops yield values too" {
    const first_square_over_20 = blk: {
        for (0..10) |i| {
            if (i * i > 20) break :blk i;
        }
        break :blk 0;
    };
    try expect(first_square_over_20 == 5);
}

break and continue affect the innermost loop by default. Label a loop to target it explicitly:

outer: for (0..5) |i| {
    for (0..5) |j| {
        if (found) break :outer;      // leaves both loops
        if (skip) continue :outer;    // next i, abandon remaining j
    }
}

The alternative in languages without this is a found flag checked after the inner loop, which is more code and easy to get subtly wrong. Being explicit about which loop also means adding a nesting level later cannot silently change what break does.