Skip to editor content
learningzig.orglesson 4 of 25

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.

Next lesson

Arrays and Structs

Learn Zig arrays and structs — create fixed-size arrays, define structs with methods, and organize data structures for clean, efficient programs.

25 min