Skip to content

Zig Control Flow

Control flow is a core concept in programming. This chapter will cover various control flow statements in Zig in detail.

if Statements

Basic if Statement

The if statement is used for conditional branching:

zig
const std = @import("std");

pub fn main() void {
    const age = 18;
    
    if (age >= 18) {
        std.debug.print("You are an adult\n", .{});
    }
    
    const temperature = 25;
    if (temperature > 30) {
        std.debug.print("It's hot\n", .{});
    } else {
        std.debug.print("The weather is nice\n", .{});
    }
}

if-else if Chain

zig
const std = @import("std");

pub fn main() void {
    const score = 85;
    
    if (score >= 90) {
        std.debug.print("Excellent\n", .{});
    } else if (score >= 80) {
        std.debug.print("Good\n", .{});
    } else if (score >= 70) {
        std.debug.print("Average\n", .{});
    } else if (score >= 60) {
        std.debug.print("Pass\n", .{});
    } else {
        std.debug.print("Fail\n", .{});
    }
}

if Expression

In Zig, if is an expression and can return a value:

zig
const std = @import("std");

pub fn main() void {
    const x = 10;
    const y = 20;
    
    // if expression
    const max = if (x > y) x else y;
    std.debug.print("Maximum: {}\n", .{max});
    
    // Complex if expression
    const status = if (x > 0) 
        "positive" 
    else if (x < 0) 
        "negative" 
    else 
        "zero";
    
    std.debug.print("Number status: {s}\n", .{status});
}

if with Optional Values

if can be used to handle optional values:

zig
const std = @import("std");

pub fn main() void {
    var maybe_number: ?i32 = 42;
    
    // Unwrap optional value
    if (maybe_number) |number| {
        std.debug.print("The number is: {}\n", .{number});
    } else {
        std.debug.print("No number\n", .{});
    }
    
    // Change to null and test again
    maybe_number = null;
    if (maybe_number) |number| {
        std.debug.print("The number is: {}\n", .{number});
    } else {
        std.debug.print("Now there's no number\n", .{});
    }
}

if with Error Union Types

zig
const std = @import("std");

const ParseError = error{
    InvalidFormat,
    OutOfRange,
};

fn parseNumber(input: []const u8) ParseError!i32 {
    if (std.mem.eql(u8, input, "42")) {
        return 42;
    } else if (std.mem.eql(u8, input, "100")) {
        return 100;
    } else if (std.mem.eql(u8, input, "999")) {
        return ParseError.OutOfRange;
    } else {
        return ParseError.InvalidFormat;
    }
}

pub fn main() void {
    const inputs = [_][]const u8{ "42", "100", "999", "abc" };
    
    for (inputs) |input| {
        if (parseNumber(input)) |number| {
            std.debug.print("Parse success: {} -> {}\n", .{ input, number });
        } else |err| {
            std.debug.print("Parse failed: {} -> {}\n", .{ input, err });
        }
    }
}

switch Statements

Basic switch Statement

The switch statement is used for multi-way branching:

zig
const std = @import("std");

pub fn main() void {
    const day = 3;
    
    switch (day) {
        1 => std.debug.print("Monday\n", .{}),
        2 => std.debug.print("Tuesday\n", .{}),
        3 => std.debug.print("Wednesday\n", .{}),
        4 => std.debug.print("Thursday\n", .{}),
        5 => std.debug.print("Friday\n", .{}),
        6, 7 => std.debug.print("Weekend\n", .{}),
        else => std.debug.print("Invalid day\n", .{}),
    }
}

switch Expression

switch can also be used as an expression:

zig
const std = @import("std");

pub fn main() void {
    const grade = 'B';
    
    const score = switch (grade) {
        'A' => 90,
        'B' => 80,
        'C' => 70,
        'D' => 60,
        'F' => 0,
        else => -1,
    };
    
    std.debug.print("Grade {} corresponds to score: {}\n", .{ grade, score });
}

Range Matching

switch supports range matching:

zig
const std = @import("std");

pub fn main() void {
    const number = 75;
    
    const category = switch (number) {
        0...59 => "Fail",
        60...69 => "Pass",
        70...79 => "Average",
        80...89 => "Good",
        90...100 => "Excellent",
        else => "Invalid score",
    };
    
    std.debug.print("Score {} is: {s}\n", .{ number, category });
}

switch with Enums

switch works very well with enums:

zig
const std = @import("std");

const Color = enum {
    Red,
    Green,
    Blue,
    Yellow,
};

fn getColorName(color: Color) []const u8 {
    return switch (color) {
        .Red => "Red",
        .Green => "Green",
        .Blue => "Blue",
        .Yellow => "Yellow",
    };
}

pub fn main() void {
    const colors = [_]Color{ .Red, .Green, .Blue, .Yellow };
    
    for (colors) |color| {
        std.debug.print("Color: {s}\n", .{getColorName(color)});
    }
}

switch with Union Types

switch can handle union types:

zig
const std = @import("std");

const Value = union(enum) {
    Integer: i32,
    Float: f64,
    String: []const u8,
    Boolean: bool,
};

fn processValue(value: Value) void {
    switch (value) {
        .Integer => |int| std.debug.print("Integer: {}\n", .{int}),
        .Float => |float| std.debug.print("Float: {d:.2}\n", .{float}),
        .String => |string| std.debug.print("String: {s}\n", .{string}),
        .Boolean => |boolean| std.debug.print("Boolean: {}\n", .{boolean}),
    }
}

pub fn main() void {
    const values = [_]Value{
        Value{ .Integer = 42 },
        Value{ .Float = 3.14 },
        Value{ .String = "Hello" },
        Value{ .Boolean = true },
    };
    
    for (values) |value| {
        processValue(value);
    }
}

Complex switch Patterns

zig
const std = @import("std");

const Point = struct {
    x: i32,
    y: i32,
};

fn analyzePoint(point: Point) []const u8 {
    return switch (point) {
        .{ .x = 0, .y = 0 } => "Origin",
        .{ .x = 0, .y = _ } => "On Y-axis",
        .{ .x = _, .y = 0 } => "On X-axis",
        .{ .x = var x, .y = var y } => blk: {
            if (x == y) {
                break :blk "On diagonal";
            } else if (x > 0 and y > 0) {
                break :blk "First quadrant";
            } else if (x < 0 and y > 0) {
                break :blk "Second quadrant";
            } else if (x < 0 and y < 0) {
                break :blk "Third quadrant";
            } else {
                break :blk "Fourth quadrant";
            }
        },
    };
}

pub fn main() void {
    const points = [_]Point{
        .{ .x = 0, .y = 0 },
        .{ .x = 0, .y = 5 },
        .{ .x = 3, .y = 0 },
        .{ .x = 2, .y = 2 },
        .{ .x = 3, .y = 4 },
        .{ .x = -2, .y = 3 },
        .{ .x = -1, .y = -1 },
        .{ .x = 2, .y = -3 },
    };
    
    for (points) |point| {
        std.debug.print("Point ({}, {}) is at: {s}\n", 
                        .{ point.x, point.y, analyzePoint(point) });
    }
}

defer Statement

Basic defer

The defer statement executes when the current scope ends:

zig
const std = @import("std");

pub fn main() void {
    std.debug.print("Start execution\n", .{});
    
    defer std.debug.print("This executes at function end\n", .{});
    
    std.debug.print("Middle execution\n", .{});
    
    // defer statement executes when function ends
}

Multiple defer Statements

Multiple defer statements execute in LIFO (Last In, First Out) order:

zig
const std = @import("std");

pub fn main() void {
    std.debug.print("Start\n", .{});
    
    defer std.debug.print("First defer\n", .{});
    defer std.debug.print("Second defer\n", .{});
    defer std.debug.print("Third defer\n", .{});
    
    std.debug.print("Middle\n", .{});
    
    // Execution order: Third defer -> Second defer -> First defer
}

Practical defer Usage

defer is commonly used for resource cleanup:

zig
const std = @import("std");

fn processFile(filename: []const u8) !void {
    std.debug.print("Opening file: {s}\n", .{filename});
    
    // Simulate file operation
    defer std.debug.print("Closing file: {s}\n", .{filename});
    
    std.debug.print("Processing file content\n", .{});
    
    // defer executes even if an error occurs
    if (std.mem.eql(u8, filename, "error.txt")) {
        return error.FileError;
    }
    
    std.debug.print("File processing complete\n", .{});
}

pub fn main() void {
    processFile("normal.txt") catch |err| {
        std.debug.print("Error processing file: {}\n", .{err});
    };
    
    std.debug.print("---\n", .{});
    
    processFile("error.txt") catch |err| {
        std.debug.print("Error processing file: {}\n", .{err});
    };
}

errdefer Statement

Basic errdefer

errdefer only executes when the function returns an error:

zig
const std = @import("std");

const ProcessError = error{
    InitializationFailed,
    ProcessingFailed,
};

fn processData(should_fail: bool) ProcessError!void {
    std.debug.print("Starting initialization\n", .{});
    
    errdefer std.debug.print("Initialization failed, performing cleanup\n", .{});
    
    if (should_fail) {
        return ProcessError.InitializationFailed;
    }
    
    std.debug.print("Initialization successful\n", .{});
    std.debug.print("Processing data\n", .{});
}

pub fn main() void {
    std.debug.print("=== Success case ===\n", .{});
    processData(false) catch |err| {
        std.debug.print("Error: {}\n", .{err});
    };
    
    std.debug.print("\n=== Failure case ===\n", .{});
    processData(true) catch |err| {
        std.debug.print("Error: {}\n", .{err});
    };
}

Combining errdefer and defer

zig
const std = @import("std");

const ResourceError = error{
    AllocationFailed,
    InitializationFailed,
};

fn allocateAndInitialize(should_fail_alloc: bool, should_fail_init: bool) ResourceError!void {
    std.debug.print("Starting resource allocation\n", .{});
    
    if (should_fail_alloc) {
        return ResourceError.AllocationFailed;
    }
    
    // Resource allocation successful
    defer std.debug.print("Releasing resource\n", .{});
    errdefer std.debug.print("Allocation succeeded but initialization failed, releasing resource\n", .{});
    
    std.debug.print("Starting initialization\n", .{});
    
    if (should_fail_init) {
        return ResourceError.InitializationFailed;
    }
    
    std.debug.print("Initialization successful\n", .{});
}

pub fn main() void {
    std.debug.print("=== Allocation failure ===\n", .{});
    allocateAndInitialize(true, false) catch |err| {
        std.debug.print("Error: {}\n", .{err});
    };
    
    std.debug.print("\n=== Initialization failure ===\n", .{});
    allocateAndInitialize(false, true) catch |err| {
        std.debug.print("Error: {}\n", .{err});
    };
    
    std.debug.print("\n=== All successful ===\n", .{});
    allocateAndInitialize(false, false) catch |err| {
        std.debug.print("Error: {}\n", .{err});
    };
}

Block Expressions

Basic Block Expression

Blocks can be used as expressions:

zig
const std = @import("std");

pub fn main() void {
    const result = blk: {
        const x = 10;
        const y = 20;
        break :blk x + y;
    };
    
    std.debug.print("Block expression result: {}\n", .{result});
}

Complex Block Expressions

zig
const std = @import("std");

fn calculateGrade(score: i32) []const u8 {
    return blk: {
        if (score >= 90) {
            break :blk "A";
        } else if (score >= 80) {
            break :blk "B";
        } else if (score >= 70) {
            break :blk "C";
        } else if (score >= 60) {
            break :blk "D";
        } else {
            break :blk "F";
        }
    };
}

pub fn main() void {
    const scores = [_]i32{ 95, 85, 75, 65, 55 };
    
    for (scores) |score| {
        const grade = calculateGrade(score);
        std.debug.print("Score {} corresponds to grade: {s}\n", .{ score, grade });
    }
}

Conditional Compilation

comptime if

Use comptime if for conditional compilation:

zig
const std = @import("std");
const builtin = @import("builtin");

pub fn main() void {
    comptime {
        if (builtin.os.tag == .windows) {
            std.debug.print("Compilation target is Windows\n", .{});
        } else if (builtin.os.tag == .linux) {
            std.debug.print("Compilation target is Linux\n", .{});
        } else if (builtin.os.tag == .macos) {
            std.debug.print("Compilation target is macOS\n", .{});
        } else {
            std.debug.print("Compilation target is other system\n", .{});
        }
    }
    
    const is_debug = comptime builtin.mode == .Debug;
    
    if (comptime is_debug) {
        std.debug.print("This is a debug build\n", .{});
    } else {
        std.debug.print("This is a release build\n", .{});
    }
}

Compile-time switch

zig
const std = @import("std");

fn getOptimizationLevel(comptime mode: std.builtin.Mode) []const u8 {
    return comptime switch (mode) {
        .Debug => "No optimization",
        .ReleaseSafe => "Safe optimization",
        .ReleaseFast => "Speed optimization",
        .ReleaseSmall => "Size optimization",
    };
}

pub fn main() void {
    const optimization = comptime getOptimizationLevel(@import("builtin").mode);
    std.debug.print("Current optimization level: {s}\n", .{optimization});
}

Control Flow Best Practices

1. Keep Conditions Simple

zig
const std = @import("std");

pub fn main() void {
    const user_age = 25;
    const has_license = true;
    const has_insurance = true;
    
    // ✅ Good practice: clear condition
    const can_drive = user_age >= 18 and has_license and has_insurance;
    
    if (can_drive) {
        std.debug.print("Can drive\n", .{});
    } else {
        std.debug.print("Cannot drive\n", .{});
    }
}

2. Prefer switch Over Long if-else Chains

zig
const std = @import("std");

const Status = enum {
    Pending,
    Processing,
    Completed,
    Failed,
};

pub fn main() void {
    const status = Status.Processing;
    
    // ✅ Good practice: use switch
    const message = switch (status) {
        .Pending => "Pending",
        .Processing => "Processing",
        .Completed => "Completed",
        .Failed => "Failed",
    };
    
    std.debug.print("Status: {s}\n", .{message});
}

3. Use defer for Resource Management

zig
const std = @import("std");

fn processWithCleanup() !void {
    std.debug.print("Acquiring resource\n", .{});
    
    // ✅ Good practice: set cleanup code immediately
    defer std.debug.print("Releasing resource\n", .{});
    
    std.debug.print("Using resource\n", .{});
    
    // Resource will be released regardless of how function exits
}

pub fn main() void {
    processWithCleanup() catch {};
}

Practice Exercises

Exercise 1: Grade Management System

zig
const std = @import("std");

const Student = struct {
    name: []const u8,
    score: i32,
};

pub fn main() void {
    const students = [_]Student{
        .{ .name = "Alice", .score = 85 },
        .{ .name = "Bob", .score = 92 },
        .{ .name = "Charlie", .score = 78 },
        .{ .name = "David", .score = 65 },
    };
    
    // TODO: Use control flow statements to complete the following tasks:
    // 1. Assign grades to each student (A: 90+, B: 80-89, C: 70-79, D: 60-69, F: <60)
    // 2. Count the number of students in each grade
    // 3. Find the students with highest and lowest scores
}

Exercise 2: Simple Calculator

zig
const std = @import("std");

const Operation = enum {
    Add,
    Subtract,
    Multiply,
    Divide,
};

pub fn main() void {
    // TODO: Implement a simple calculator
    // 1. Use switch to handle different operations
    // 2. Handle division by zero error
    // 3. Use defer for resource cleanup
}

Exercise 3: State Machine

zig
const std = @import("std");

const State = enum {
    Idle,
    Running,
    Paused,
    Stopped,
};

const Event = enum {
    Start,
    Pause,
    Resume,
    Stop,
};

pub fn main() void {
    // TODO: Implement a simple state machine
    // 1. Based on current state and event, determine next state
    // 2. Use switch to handle state transitions
    // 3. Handle invalid state transitions
}

Summary

This chapter covered Zig control flow statements in detail:

  • if statements: conditional branching, supports expression form
  • switch statements: multi-way branching, powerful features
  • defer and errdefer: resource management and cleanup
  • ✅ Block expressions: using code blocks as expressions
  • ✅ Conditional compilation: compile-time control flow
  • ✅ Best practices and practical applications

Mastering control flow is fundamental to writing complex program logic. In the next chapter, we'll learn about Zig's error handling mechanisms.

Content is for learning and research only.