⚡ Zig Guide LiveUnofficialbut fully verified
✓ Zig 0.17.0-dev.2122+3e15e99e6What's newOn an older Zig?

Loops as Expressions

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

fn firstIndexOf(haystack: []const u8, needle: u8) ?usize {
    // The `else` branch runs only if the loop finished without breaking,
    // so "not found" cannot be forgotten.
    return for (haystack, 0..) |c, i| {
        if (c == needle) break i;
    } else null;
}

test "for as an expression" {
    try expect(firstIndexOf("hello", 'l').? == 2);
    try expect(firstIndexOf("hello", 'z') == null);
}

test "while as an expression" {
    var i: u32 = 0;
    const result = while (i < 10) : (i += 1) {
        if (i == 4) break i * 10;
    } else 0;
    try expect(result == 40);
}

test "the else branch runs when nothing breaks" {
    var i: u32 = 0;
    const result = while (i < 3) : (i += 1) {
        if (i == 99) break @as(u32, 1);
    } else @as(u32, 2);
    try expect(result == 2);
}

A loop can produce a value: break x yields x, and the else branch supplies the value when the loop finishes without ever breaking.

return for (haystack, 0..) |c, i| {
    if (c == needle) break i;
} else null;

That is a complete linear search, and the “not found” case is structurally impossible to forget: leave off the else and it does not compile.

Compare the usual alternative:

var result: ?usize = null;
for (haystack, 0..) |c, i| {
    if (c == needle) { result = i; break; }
}
return result;

Same behaviour, but now result is mutable, lives longer than it needs to, and nothing forces you to initialise it correctly.

What else means on a loop

It is not the else of an if. It runs when the loop ends normally: the for ran out of elements, or the while condition became false. A break skips it. That is the entire rule, and it is what makes the construct safe for searches. There are exactly two ways out of the loop, and each one produces a value.

The third test in the snippet pins that down with a break written so it can never fire:

const result = while (i < 3) : (i += 1) {
    if (i == 99) break @as(u32, 1);
} else @as(u32, 2);

i never reaches 99, the condition goes false, and result is 2.

What the missing else actually looks like

The failure is worth seeing, because the error does not mention else at all. A loop with no else has nothing to evaluate to when it ends normally, so its type is void:

error: expected type '?usize', found 'void'

pointing at the whole for expression. Read it as “this loop does not always produce a value”. Adding the else is the fix.

The one loop that needs no else is one that cannot end normally. while (true) has no condition to go false, so the only way out is break, and the compiler knows it:

const v = while (true) : (i += 1) {
    if (i == 3) break i;
};

That compiles, and v is usize. Add an else there and it is dead code.

Both arms must agree on a type

The break value and the else value are two arms of one expression, so they have to reach a single type. In the search above they are usize and null, which unify as ?usize, and that is the function’s return type.

When the arms are bare integer literals they are comptime-known and coerce to whatever the surrounding code wants. When there is nothing to coerce toward, you have to say. That is why the test writes @as(u32, 1) and @as(u32, 2) rather than 1 and 2: const result = ... gives the literals no target type to take their size and signedness from.

If the arms genuinely disagree, the error names both types and points at the break. That check is the reason a search cannot accidentally return an index on one path and a boolean on another.

When to reach for it

The pattern is worth using when the loop answers exactly one question: find this, count until that, decide whether any element qualifies. The value comes out where it is decided, and the compiler holds you to covering both outcomes.

It stops helping when a loop has five break sites carrying five different meanings. Nothing prevents that, and the result is harder to follow than an explicit variable, because the reader now has to find every break to know what the expression can be. One or two exits is the shape this is for.