Skip to content

Labels and Block Expressions

Zig has blocks.

Zig has blocks.

A block is a group of statements surrounded by braces:

{
    const x = 10;
    const y = 20;
    _ = x + y;
}

You have already seen blocks in if, switch, while, and for.

if (age >= 18) {
    std.debug.print("adult\n", .{});
}

The part inside {} is a block.

Most beginner code uses blocks only to group statements. Zig can do more than that. In Zig, a block can also produce a value.

A Block Creates a Scope

A block creates a new scope.

A scope is a region where names exist.

const std = @import("std");

pub fn main() void {
    {
        const x = 10;
        std.debug.print("x = {}\n", .{x});
    }

    // x cannot be used here
}

The name x exists only inside the inner block.

This is invalid:

pub fn main() void {
    {
        const x = 10;
    }

    std.debug.print("x = {}\n", .{x}); // error
}

The variable x is gone after the block ends.

This is useful because it lets you keep temporary names local. Small scopes make code easier to read.

Why Blocks Matter

A block lets you group several steps.

const area = blk: {
    const width = 10;
    const height = 20;
    break :blk width * height;
};

This computes an area and stores it in area.

The block contains temporary variables:

const width = 10;
const height = 20;

Then it returns a value:

break :blk width * height;

The final value of the block is:

200

So this:

const area = blk: {
    const width = 10;
    const height = 20;
    break :blk width * height;
};

means roughly:

const area = 200;

But the block allowed us to use multiple statements to compute the value.

Labels

A label gives a name to a block.

blk: {
    // block body
}

Here, the label is:

blk

The full shape is:

label_name: {
    // statements
}

A label is useful because break can refer to it.

const value = blk: {
    break :blk 123;
};

This means:

leave the block named blk, and make its value 123.

break from a Block

In Zig, break is not only for loops. It can also leave a labeled block.

const std = @import("std");

pub fn main() void {
    const value = blk: {
        break :blk 42;
    };

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

This prints:

value = 42

The important line is:

break :blk 42;

Read it as:

break out of the block named blk with the value 42.

The type of value is inferred from the value returned by the block.

Blocks Without Values

A block does not always need to produce a value.

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

This block is only used for grouping.

Its type is void.

void means “no useful value.”

This is also void:

const result = {};

But in normal code, you rarely write that. Most void blocks are used for control flow or grouping.

Block Expressions in if

Blocks become more useful when combined with if.

const std = @import("std");

pub fn main() void {
    const score = 82;

    const message = if (score >= 60) blk: {
        const extra = score - 60;
        break :blk if (extra >= 20) "strong pass" else "pass";
    } else blk: {
        break :blk "fail";
    };

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

This prints:

strong pass

Each branch uses a block to compute a value.

The first branch:

blk: {
    const extra = score - 60;
    break :blk if (extra >= 20) "strong pass" else "pass";
}

has a temporary variable extra.

The block returns either "strong pass" or "pass".

Each Branch Must Return a Compatible Type

When blocks are used as values, their returned values must fit the expected type.

This is valid:

const value = blk: {
    break :blk 10;
};

This is also valid:

const message = if (ok) blk: {
    break :blk "yes";
} else blk: {
    break :blk "no";
};

Both branches return strings.

This is invalid:

const value = if (ok) blk: {
    break :blk 10;
} else blk: {
    break :blk "no";
};

One branch returns an integer. The other returns a string. Zig rejects this because value cannot have two unrelated types.

Labels on Loops

Labels can also be used with loops.

This matters when you have nested loops.

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:

stop the loop named outer.

Without the label, break would stop only the inner loop.

continue with Labels

Labels also work with continue.

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

continue :outer;

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

This is useful, but use it carefully. Labeled continue can make code harder to follow if overused.

Choosing Label Names

For small block expressions, many Zig examples use blk.

const value = blk: {
    break :blk 123;
};

For loops, use names that explain the purpose.

search: for (items) |item| {
    // ...
}
rows: for (grid) |row| {
    // ...
}
commands: while (true) {
    // ...
}

A label should help the reader understand what is being exited or continued.

When Block Expressions Are Useful

Block expressions are useful when a value needs several steps to compute.

Instead of writing:

var result: u32 = undefined;

if (x > 10) {
    result = x * 2;
} else {
    result = x + 1;
}

You can write:

const result = if (x > 10) x * 2 else x + 1;

If the computation needs multiple statements, use blocks:

const result = if (x > 10) blk: {
    const doubled = x * 2;
    break :blk doubled;
} else blk: {
    const incremented = x + 1;
    break :blk incremented;
};

This keeps result as a const. That is often cleaner than creating a mutable variable and assigning it later.

A Practical Example

Suppose we want to classify a file size.

const std = @import("std");

pub fn main() void {
    const bytes: u64 = 2_500_000;

    const label = blk: {
        if (bytes < 1_000) {
            break :blk "small";
        }

        if (bytes < 1_000_000) {
            break :blk "medium";
        }

        break :blk "large";
    };

    std.debug.print("file is {s}\n", .{label});
}

This prints:

file is large

The block allows several checks, but the final result is still a single constant:

const label = ...

That is a clean Zig pattern.

The Main Idea

A block groups statements and creates a scope.

A labeled block can also produce a value:

const value = name: {
    break :name 123;
};

Labels can also name loops, which lets break and continue target an outer loop.

For beginners, the most important uses are:

const result = blk: {
    const x = 10;
    const y = 20;
    break :blk x + y;
};

and:

outer: for (items) |item| {
    for (subitems) |subitem| {
        break :outer;
    }
}

Blocks and labels give Zig precise control flow without hidden behavior.