Skip to content

`break` and `continue`

Loops repeat code. But sometimes you do not want a loop to finish in the normal way.

break and continue

Loops repeat code. But sometimes you do not want a loop to finish in the normal way.

You may want to stop early.

You may want to skip one item.

You may want to leave an outer loop from inside an inner loop.

Zig gives you two simple tools for this:

break

and:

continue

break exits a loop.

continue skips to the next loop iteration.

break Stops a Loop

Here is a simple loop:

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

The loop would normally continue until i reaches 10.

But this branch stops it early:

if (i == 4) {
    break;
}

When i becomes 4, break runs. The loop ends immediately.

The print statement does not run for 4.

break in a for Loop

break works in for loops too.

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

The loop stops when it reaches 30.

This is useful for search logic. Once you find what you want, there is no reason to keep looping.

continue Skips One Iteration

continue does not stop the whole loop.

It skips the rest of the current iteration and moves to the next one.

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, this line runs:

continue;

The rest of the loop body is skipped.

So this line does not run for 3:

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

Then the loop continues with the next value.

continue in a while Loop with a Continue Expression

Remember this form:

while (condition) : (update) {
    // body
}

The update part still runs after continue.

Example:

const std = @import("std");

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

    while (i < 5) : (i += 1) {
        if (i == 2) {
            continue;
        }

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

This prints:

0
1
3
4

When i == 2, continue runs. Zig skips the print, then runs:

i += 1

So the loop still moves forward.

This is important. Without the continue expression, you could accidentally create an infinite loop.

A Common Mistake

This loop never ends:

var i: u8 = 0;

while (i < 5) {
    if (i == 2) {
        continue;
    }

    i += 1;
}

When i becomes 2, the loop reaches continue.

That skips:

i += 1;

So i stays 2 forever.

A safer version is:

var i: u8 = 0;

while (i < 5) : (i += 1) {
    if (i == 2) {
        continue;
    }
}

Now the update happens even when continue is used.

Skipping Items with continue

continue is useful when some items should be ignored.

const std = @import("std");

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

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

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

This prints:

1
3
5

The expression:

n % 2 == 0

checks whether n is even.

If it is even, the loop skips it.

So only odd numbers are printed.

Searching with break

break is useful when searching.

const std = @import("std");

pub fn main() void {
    const numbers = [_]u8{ 4, 8, 15, 16, 23, 42 };
    const target: u8 = 16;

    var found = false;

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

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

This prints:

found = true

The loop stops as soon as it finds the target.

That is better than checking the remaining items for no reason.

break Can Return a Value

In Zig, loops can be expressions.

That means break can return a value from a loop.

const std = @import("std");

pub fn main() void {
    const numbers = [_]u8{ 4, 8, 15, 16, 23, 42 };
    const target: u8 = 16;

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

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

This prints:

found = true

Read this part carefully:

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

It means:

loop through the numbers

if target is found, stop and make the loop value true

if the loop finishes normally, use the else value false

So found becomes either true or false.

This is a clean Zig pattern for search code.

break Value Must Match the else Value

When a loop returns a value, the break value and the else value must have compatible types.

This is valid:

const result = for (items) |item| {
    if (item == target) {
        break true;
    }
} else false;

Both values are booleans.

This is invalid:

const result = for (items) |item| {
    if (item == target) {
        break true;
    }
} else "not found";

One value is a boolean. The other is a string.

Zig rejects this because result needs one clear type.

break from a Labeled Block

break can also leave a labeled block.

const std = @import("std");

pub fn main() void {
    const value = blk: {
        if (true) {
            break :blk 123;
        }

        break :blk 0;
    };

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

This prints:

value = 123

The label is:

blk:

The line:

break :blk 123;

means:

leave the block named blk and make its value 123.

This is not a loop. It is a labeled block expression.

break from an Outer Loop

Without labels, break exits the nearest loop.

for (0..3) |i| {
    for (0..3) |j| {
        if (j == 1) {
            break;
        }

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

This prints:

i=0, j=0
i=1, j=0
i=2, j=0

The break exits only the inner loop.

To exit the outer loop, use a label:

const std = @import("std");

pub fn main() void {
    outer: for (0..3) |i| {
        for (0..3) |j| {
            if (i == 1 and j == 1) {
                break :outer;
            }

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

This prints:

i=0, j=0
i=0, j=1
i=0, j=2
i=1, j=0

The label is:

outer:

The line:

break :outer;

means:

leave the loop named outer.

continue to an Outer Loop

A labeled continue moves to the next iteration of a named loop.

const std = @import("std");

pub fn main() void {
    outer: for (0..3) |i| {
        for (0..3) |j| {
            if (j == 1) {
                continue :outer;
            }

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

This prints:

i=0, j=0
i=1, j=0
i=2, j=0

When j == 1, this runs:

continue :outer;

That skips the rest of the inner loop and starts the next iteration of the outer loop.

Use labeled continue only when it makes the code clearer. In many cases, a helper function is easier to read.

Early Exit for Validation

break and continue are useful in validation code.

Suppose we want to check that all bytes are lowercase ASCII letters.

const std = @import("std");

fn isLowercaseAscii(text: []const u8) bool {
    for (text) |byte| {
        if (byte < 'a' or byte > 'z') {
            return false;
        }
    }

    return true;
}

pub fn main() void {
    const ok = isLowercaseAscii("zig");

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

This prints:

ok = true

This example uses return, not break, because the loop is inside a function and the whole function can stop immediately.

But the idea is the same: once you know the answer, exit early.

Prefer Clear Exits

Early exits are useful, but too many can make code hard to follow.

This is usually clear:

for (items) |item| {
    if (!item.valid) {
        continue;
    }

    process(item);
}

The continue removes an unwanted case early.

This is also clear:

for (items) |item| {
    if (item.id == target_id) {
        break;
    }
}

The break stops after finding the target.

But deeply nested code with many labels can become difficult:

outer: for (a) |x| {
    middle: for (b) |y| {
        inner: for (c) |z| {
            if (some_condition) {
                break :outer;
            }
        }
    }
}

Sometimes this is correct. But often, a small helper function is better.

The Main Idea

break exits a loop.

continue skips the current iteration and moves to the next one.

Use break when the loop has done enough work.

Use continue when the current item should be ignored.

Use labels when you need to control an outer loop.

The beginner rule is simple: keep loop exits obvious. A reader should be able to see quickly why the loop stops or why an item is skipped.