Skip to lesson

learningzig.org / basics / 04-control-flow · lesson 4 of 25

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 like i32, Zig rejects i % 2 at compile time and asks you to pick your rounding behaviour explicitly: @mod follows the sign of the divisor (result always non-negative here), while @rem follows 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:

  1. 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"
  2. Create a loop that calculates the factorial of a number
  3. Use a switch statement to convert a month number (1-12) to its name
  4. Write nested loops to print a multiplication table
  5. 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 break to exit loops early and continue to 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.