TL;DR
Master Zig control flow — use if expressions, while and for loops, and switch expressions with pattern matching to write concise, safe logic.
Key concepts
- zig control flow
- zig switch expression
- zig loops
- zig if statement
- zig pattern matching
Control Flow
Control flow statements let you make decisions and repeat actions in your programs. In this lesson, we'll explore Zig's if statements, loops, and switch expressions.
What You'll Learn
- How to use if/else statements for conditional logic
- Different types of loops: while, for, and labeled loops
- Switch expressions for pattern matching
- How to use break and continue in loops
- Error handling with control flow
If Statements
Zig's if statements are straightforward but powerful:
const std = @import("std");
pub fn main() !void {
const temperature: i32 = 25;
// Basic if statement
if (temperature > 30) {
std.debug.print("It's hot outside!\n", .{});
}
// If-else
if (temperature < 0) {
std.debug.print("It's freezing!\n", .{});
} else {
std.debug.print("Temperature is above freezing\n", .{});
}
// If-else if-else chain
if (temperature < 0) {
std.debug.print("Freezing\n", .{});
} else if (temperature < 10) {
std.debug.print("Cold\n", .{});
} else if (temperature < 20) {
std.debug.print("Mild\n", .{});
} else {
std.debug.print("Warm\n", .{});
}
}
If as an Expression
In Zig, if can be used as an expression to return values:
const std = @import("std");
pub fn main() !void {
const score: i32 = 85;
// If as an expression
const grade = if (score >= 90)
"A"
else if (score >= 80)
"B"
else if (score >= 70)
"C"
else if (score >= 60)
"D"
else
"F";
std.debug.print("Score: {}, Grade: {s}\n", .{score, grade});
// If with optional values
const maybe_value: ?i32 = 42;
if (maybe_value) |value| {
std.debug.print("Value exists: {}\n", .{value});
} else {
std.debug.print("Value is null\n", .{});
}
}
While Loops
While loops continue executing as long as a condition is true:
const std = @import("std");
pub fn main() !void {
// Basic while loop
var count: i32 = 0;
while (count < 5) {
std.debug.print("Count: {}\n", .{count});
count += 1;
}
// While with continue expression
var i: i32 = 0;
while (i < 10) : (i += 1) {
// On signed integers the `%` operator is rejected; use @mod (or @rem)
if (@mod(i, 2) == 0) {
std.debug.print("{} is even\n", .{i});
}
}
// While with optional (loops until null)
var optional: ?i32 = 5;
while (optional) |value| {
std.debug.print("Value: {}\n", .{value});
optional = if (value > 0) value - 1 else null;
}
}
Signed remainder: The
%operator only works on unsigned integers and non-negative comptime values. For a signed integer likei32, Zig rejectsi % 2at compile time and asks you to pick your rounding behaviour explicitly:@modfollows the sign of the divisor (result always non-negative here), while@remfollows the sign of the dividend. We use@mod(i, 2)above to test for even numbers.
For Loops
For loops iterate over ranges and slices:
const std = @import("std");
pub fn main() !void {
// Loop over a range
for (0..5) |i| {
std.debug.print("Index: {}\n", .{i});
}
// Loop over an array
const numbers = [_]i32{10, 20, 30, 40, 50};
for (numbers) |num| {
std.debug.print("Number: {}\n", .{num});
}
// Loop with index
const fruits = [_][]const u8{"apple", "banana", "cherry"};
for (fruits, 0..) |fruit, idx| {
std.debug.print("{}: {s}\n", .{idx, fruit});
}
// Loop over a string (slice of bytes)
const message = "Hello";
for (message) |char| {
std.debug.print("{c} ", .{char});
}
std.debug.print("\n", .{});
}
Break and Continue
Control loop execution with break and continue:
const std = @import("std");
pub fn main() !void {
// Using break
var i: i32 = 0;
while (true) {
if (i >= 5) {
break; // Exit the loop
}
std.debug.print("{} ", .{i});
i += 1;
}
std.debug.print("\n", .{});
// Using continue
for (0..10) |num| {
if (num % 2 == 0) {
continue; // Skip even numbers
}
std.debug.print("{} ", .{num}); // Only prints odd numbers
}
std.debug.print("\n", .{});
// Break with value (returning from loop)
const result = for (0..10) |n| {
if (n * n > 50) {
break n; // Return the value
}
} else 0; // Default value if loop completes without break
std.debug.print("First n where n^2 > 50: {}\n", .{result});
// Try modifying these loops!
}
Switch Expressions
Switch is a powerful pattern matching feature in Zig:
const std = @import("std");
pub fn main() !void {
// Basic switch
const day: u8 = 3;
const day_name = switch (day) {
1 => "Monday",
2 => "Tuesday",
3 => "Wednesday",
4 => "Thursday",
5 => "Friday",
6 => "Saturday",
7 => "Sunday",
else => "Invalid day",
};
std.debug.print("Day {}: {s}\n", .{day, day_name});
// Switch with multiple values
const grade: u8 = 85;
const performance = switch (grade) {
90...100 => "Excellent",
80...89 => "Good",
70...79 => "Average",
60...69 => "Below Average",
else => "Failing",
};
std.debug.print("Grade {}: {s}\n", .{grade, performance});
// Switch with enums
const Color = enum { red, green, blue, yellow };
const color = Color.green;
const hex = switch (color) {
.red => "#FF0000",
.green => "#00FF00",
.blue => "#0000FF",
.yellow => "#FFFF00",
};
std.debug.print("Color {s}: {s}\n", .{@tagName(color), hex});
}
Labeled Blocks and Loops
You can label blocks and loops to control flow more precisely:
const std = @import("std");
pub fn main() !void {
// Labeled break
outer: for (0..5) |i| {
for (0..5) |j| {
if (i * j > 6) {
std.debug.print("Breaking at i={}, j={}\n", .{i, j});
break :outer; // Break out of outer loop
}
std.debug.print("({}, {}) ", .{i, j});
}
std.debug.print("\n", .{});
}
// Labeled continue
outer2: for (0..3) |i| {
for (0..3) |j| {
if (j == 1) {
continue :outer2; // Continue outer loop
}
std.debug.print("({}, {}) ", .{i, j});
}
std.debug.print("\n", .{});
}
// Labeled blocks (return values from blocks)
const result = blk: {
const x = 10;
const y = 20;
break :blk x + y;
};
std.debug.print("Result from block: {}\n", .{result});
}
Error Handling in Control Flow
Combine control flow with error handling:
const std = @import("std");
const ValidationError = error{TooSmall, TooBig};
fn validateAge(age: i32) ValidationError!void {
if (age < 0) {
return ValidationError.TooSmall;
}
if (age > 150) {
return ValidationError.TooBig;
}
}
pub fn main() !void {
const ages = [_]i32{-5, 25, 200, 45};
for (ages) |age| {
// If statement with error handling
if (validateAge(age)) {
std.debug.print("Age {} is valid\n", .{age});
} else |err| {
std.debug.print("Age {} is invalid: {}\n", .{age, err});
}
}
}
Practice Exercises
Try these exercises:
- Write a program that prints numbers 1-100, but for multiples of 3 print "Fizz", for multiples of 5 print "Buzz", and for multiples of both print "FizzBuzz"
- Create a loop that calculates the factorial of a number
- Use a switch statement to convert a month number (1-12) to its name
- Write nested loops to print a multiplication table
- Create a function that finds the first prime number greater than a given number using a loop and break
Challenge: Write a function that determines if a number is prime using loops and conditional statements. Test it with numbers from 2 to 30.
Key Takeaways
- If statements can be used as expressions to return values
- While loops continue until a condition is false; for loops iterate over ranges and slices
- Use
breakto exit loops early andcontinueto skip to the next iteration - Switch expressions provide exhaustive pattern matching
- Labeled blocks and loops allow precise control flow in nested structures
- Control flow works seamlessly with error handling using if-else with errors
Next Steps
Now that you understand control flow, let's explore Zig's data structures including arrays and structs.
Two-tier handoff: this document is the complete reading surface. Continue learning for stateful practice, progress, and real sandbox execution.