Zig If Else

Programs make decisions based on conditions. An if statement checks a condition and runs a block of code only when that condition is true. The else branch runs when the condition is false. Zig's if-else works like most languages, with a few unique abilities such as returning values from if expressions.

Basic If Statement

const temperature: i32 = 38;

if (temperature > 37) {
    std.debug.print("You have a fever.\n", .{});
}
  temperature = 38
        |
  [ 38 > 37 ? ]
        |
     true → "You have a fever."
     false → (nothing happens)

The parentheses around the condition are required in Zig. The condition must be a boolean — Zig does not treat numbers as truthy or falsy the way some other languages do. Writing if (count) causes a compile error. Write if (count != 0) instead.

If-Else

const score: i32 = 45;

if (score >= 50) {
    std.debug.print("Pass\n", .{});
} else {
    std.debug.print("Fail\n", .{});
}
  score = 45
      |
  [ 45 >= 50 ? ]
      |
   true → "Pass"
   false → "Fail"   ← this runs

If-Else If-Else Chain

const marks: i32 = 72;

if (marks >= 90) {
    std.debug.print("Grade: A\n", .{});
} else if (marks >= 75) {
    std.debug.print("Grade: B\n", .{});
} else if (marks >= 60) {
    std.debug.print("Grade: C\n", .{});
} else {
    std.debug.print("Grade: F\n", .{});
}
  marks = 72
      |
  [ >= 90 ? ] → false
      |
  [ >= 75 ? ] → false
      |
  [ >= 60 ? ] → true → "Grade: C"

Zig checks conditions top to bottom and stops at the first true one. The remaining branches do not run. Put the most specific or most likely conditions at the top for cleaner logic.

If as an Expression

Zig's if statement can return a value, making it an expression. This removes the need for temporary variables:

const speed: i32 = 120;
const status = if (speed > 100) "fast" else "normal";
std.debug.print("Speed status: {s}\n", .{status});
  speed = 120
      |
  [ 120 > 100 ? ]
      |
   true  → status = "fast"
   false → status = "normal"

Both branches must return the same type. If one branch returns a string and the other returns an integer, Zig produces a compile error. This rule keeps your code predictable.

Nested If Statements

const has_ticket: bool = true;
const age: u8 = 15;

if (has_ticket) {
    if (age >= 18) {
        std.debug.print("Adult entry granted.\n", .{});
    } else {
        std.debug.print("Child entry granted.\n", .{});
    }
} else {
    std.debug.print("No ticket. Entry denied.\n", .{});
}
  [ Has ticket? ]
       |
    true → [ Age >= 18? ]
                |
             true  → "Adult entry"
             false → "Child entry"
    false → "Entry denied"

Nesting beyond two levels hurts readability. When you find deeply nested conditions, consider restructuring with early returns or combining conditions using and / or.

Combining Conditions

const is_weekend: bool = true;
const is_sunny: bool = true;

if (is_weekend and is_sunny) {
    std.debug.print("Go to the park!\n", .{});
} else if (is_weekend or is_sunny) {
    std.debug.print("Partially good day.\n", .{});
} else {
    std.debug.print("Stay indoors.\n", .{});
}

If with Optional Capture

Zig has optional types — values that may or may not exist (covered in depth later). The if statement can unwrap an optional and give you the inner value directly:

const maybe_score: ?i32 = 88;

if (maybe_score) |actual_score| {
    std.debug.print("Score is {d}\n", .{actual_score});
} else {
    std.debug.print("No score recorded.\n", .{});
}
  maybe_score = ?i32
        |
  [ Has a value? ]
        |
     yes → capture into actual_score → use it
     no  → run else block

The |actual_score| syntax captures the unwrapped value. Inside the block, actual_score is a plain i32 — no question mark, no unwrapping needed. This pattern prevents null pointer errors because you cannot use the value without first confirming it exists.

If with Error Union Capture

Similarly, if a function returns a result that might be an error, you can capture the success value:

const result: anyerror!i32 = 42;

if (result) |value| {
    std.debug.print("Got value: {d}\n", .{value});
} else |err| {
    std.debug.print("Error: {}\n", .{err});
}

Practical Example: Login Check

const std = @import("std");

pub fn main() void {
    const username = "admin";
    const password = "secret123";
    const entered_user = "admin";
    const entered_pass = "secret123";

    if (std.mem.eql(u8, username, entered_user) and
        std.mem.eql(u8, password, entered_pass))
    {
        std.debug.print("Login successful.\n", .{});
    } else {
        std.debug.print("Invalid credentials.\n", .{});
    }
}

std.mem.eql(u8, a, b) compares two strings byte by byte and returns true if they match. Comparing strings with == in Zig compares pointer addresses, not content, so always use std.mem.eql for string comparison.

Comptime If

Zig can evaluate if statements at compile time using comptime:

const IS_DEBUG = true;

comptime {
    if (IS_DEBUG) {
        @compileLog("Debug build active");
    }
}

Comptime if statements remove dead branches entirely during compilation. Code in the false branch produces no machine code at all — not even a check at runtime. This is how Zig replaces C preprocessor #ifdef directives with regular code.

Leave a Comment

Your email address will not be published. Required fields are marked *