Control Flow
if, switch, while, and for are expressions, not just statements. The compiler enforces exhaustiveness and — in safe builds — never lets a check slip past at runtime.
if Expressions
if does not need parentheses in Zig. Every branch is a block expression: the value of the taken branch becomes the value of the whole if, which is how Zig avoids the ternary-operator mess.
Figure 1 — if/else decision: one branch is always taken; the result is a value.
Basics
const grade: u8 = 75;
const label = if (grade >= 60) "pass" else "fail";
// if-else as an expression — no ternary, just an if that yields a value
std.debug.print("{s}\n", .{label});
Unwrapping Optionals
Of special note: if can capture the value inside an optional in one step; you will formalize this in the optionals lesson.
const maybe: ?u32 = 42;
if (maybe) |value| {
std.debug.print("value is {d}\n", .{value}); // captured, safe
} else {
std.debug.print("no value\n", .{});
}
switch Expressions
switch compares a value against prongs. It is guaranteed exhaustive: the compiler refuses a switch that could miss a case, which eliminates entire bug families around missed enum values.
Basics and Exhaustiveness
const kind = 2;
const name = switch (kind) {
0 => "zero",
1 => "one",
else => "many", // else covers everything else — required here
};
std.debug.print("{s}\n", .{name});
Figure 2 — switch fans out to exactly one prong; exhaustive switching means no case is forgotten.
Switching on Enums
With enumerations the compiler knows every possible value, so the else prong becomes unnecessary and a forgotten case is a compile error. You will build the full picture in the structures lesson.
while Loops
while repeats its block as long as the condition is true. Two Zig extensions make it far more expressive than C: a continue expression that runs after each iteration, and direct support for optional and error-union conditions.
Basics
var i: u32 = 0;
while (i < 5) {
std.debug.print("{d}\n", .{i});
i += 1;
}
Continue Expression
var sum: u32 = 0;
var n: u32 = 0;
// the : (n += 1) part runs after each body iteration, before rechecking
while (n < 5) : (n += 1) {
sum += n;
}
// sum == 10
Figure 3 — the condition guards every entry to the loop body.
while with Error Unions
When the condition is an error-union expression, while captures the unwrapped value; an error terminates the loop, and an else prong can handle it. This pattern drives streaming readers.
var lines_read: u32 = 0;
// readLine returns an error union; the while loop unwraps each success
while (readLine()) |line| {
_ = line;
lines_read += 1;
} else |err| {
std.debug.print("stopped with error: {s}\n", .{@errorName(err)});
}
for Loops
for iterates over arrays and slices directly. It cannot run away into a manual index — the iteration is bounded by the collection itself, and you can capture the index as a second variable.
Basics
const names = [_][]const u8{ "Ada", "Guido", "Kern" };
for (names) |name| {
std.debug.print("{s}\n", .{name});
}
Indexed Iteration
const squares = [_]u32{ 1, 4, 9, 16 };
for (squares, 0..) |square, index| {
std.debug.print("[{d}] = {d}\n", .{ index, square });
}
Break, Continue, and Labels
Inside loops, break exits and continue jumps to the next iteration. Labels name a surrounding loop so a nested break or continue targets the right one — Zig rejects ambiguous jumps.
Labels
outer: for (0..5) |x| {
for (0..5) |y| {
if (y == 2) continue :outer; // skip to next x
if (x == 4) break :outer; // leave both loops
}
}
defer — Cleanup Control Flow
A special, purely-Zig form of control flow: defer schedules cleanup to run when the surrounding scope exits, even on early returns. It is the backbone of resource management and is studied in the functions lesson.
Common Pitfalls
Missing else in Expressions
An if used as an expression must have both branches; a bare statement if needs none. Mixing the two confuses newcomers — decide up front whether you are computing a value or just acting.
Runaway while Loops
while has no automatic counter; forgetting the continue expression leaves a busy loop. In safe builds a runaway loop is diagnosable; better, test your loops with zig test (see the testing lesson).
Next: Functions & Cleanup — parameters, returns, defer, and errdefer.