Skip to content

`if` Expressions

Programs need to make choices.

if Expressions

Programs need to make choices.

A program may need to ask:

Should I print this message?

Should I open this file?

Should I stop because something went wrong?

Should I use this value or another value?

In Zig, the simplest way to make a choice is with if.

const std = @import("std");

pub fn main() void {
    const age = 20;

    if (age >= 18) {
        std.debug.print("adult\n", .{});
    }
}

This program prints:

adult

The condition is:

age >= 18

This condition has type bool. A bool value is either true or false.

If the condition is true, Zig runs the code inside the braces.

If the condition is false, Zig skips it.

The Basic Shape

The basic shape of if is:

if (condition) {
    // code runs when condition is true
}

The condition must be inside parentheses.

The body must be a block. A block begins with { and ends with }.

Example:

const is_debug = true;

if (is_debug) {
    std.debug.print("debug mode\n", .{});
}

Here, is_debug is already a boolean value, so it can be used directly as the condition.

if with else

Most choices have two paths.

const std = @import("std");

pub fn main() void {
    const score = 75;

    if (score >= 60) {
        std.debug.print("pass\n", .{});
    } else {
        std.debug.print("fail\n", .{});
    }
}

This program prints:

pass

The else block runs only when the if condition is false.

The shape is:

if (condition) {
    // true path
} else {
    // false path
}

Only one branch runs.

If the condition is true, Zig runs the first block and skips the else block.

If the condition is false, Zig skips the first block and runs the else block.

if with else if

Sometimes there are more than two cases.

const std = @import("std");

pub fn main() void {
    const score = 82;

    if (score >= 90) {
        std.debug.print("grade A\n", .{});
    } else if (score >= 80) {
        std.debug.print("grade B\n", .{});
    } else if (score >= 70) {
        std.debug.print("grade C\n", .{});
    } else {
        std.debug.print("grade D or below\n", .{});
    }
}

This program prints:

grade B

Zig checks the conditions from top to bottom.

First:

score >= 90

That is false.

Then:

score >= 80

That is true, so Zig runs that block and stops checking the rest.

The later branches are skipped.

Order matters. This version is correct:

if (score >= 90) {
    std.debug.print("grade A\n", .{});
} else if (score >= 80) {
    std.debug.print("grade B\n", .{});
}

This version is wrong for grading:

if (score >= 80) {
    std.debug.print("grade B\n", .{});
} else if (score >= 90) {
    std.debug.print("grade A\n", .{});
}

A score of 95 is also greater than 80, so the first branch runs. The score >= 90 branch is never reached.

Conditions Must Be Boolean

Zig is strict about conditions.

In many languages, numbers can act like booleans. For example, 0 may mean false and 1 may mean true.

Zig does not do that.

This is invalid:

const n = 1;

if (n) {
    // invalid Zig
}

n is an integer, not a boolean.

You must write the condition directly:

const n = 1;

if (n != 0) {
    std.debug.print("n is not zero\n", .{});
}

This is one of Zig’s important habits: make the test explicit.

Instead of asking Zig to guess what you mean, say exactly what you mean.

Common Comparison Operators

You often build if conditions with comparison operators.

OperatorMeaning
==equal to
!=not equal to
<less than
<=less than or equal to
>greater than
>=greater than or equal to

Example:

const x = 10;

if (x == 10) {
    std.debug.print("x is 10\n", .{});
}

if (x != 0) {
    std.debug.print("x is not zero\n", .{});
}

if (x > 5) {
    std.debug.print("x is greater than 5\n", .{});
}

Each comparison produces a bool.

Combining Conditions

You can combine boolean conditions.

Use and when both conditions must be true.

const age = 25;
const has_ticket = true;

if (age >= 18 and has_ticket) {
    std.debug.print("you may enter\n", .{});
}

Use or when at least one condition must be true.

const is_admin = false;
const is_owner = true;

if (is_admin or is_owner) {
    std.debug.print("access allowed\n", .{});
}

Use ! to reverse a boolean.

const logged_in = false;

if (!logged_in) {
    std.debug.print("please log in\n", .{});
}

Read !logged_in as “not logged in.”

if Is an Expression

In Zig, if can produce a value.

That means you can write:

const std = @import("std");

pub fn main() void {
    const age = 20;

    const message = if (age >= 18) "adult" else "minor";

    std.debug.print("{s}\n", .{message});
}

This program prints:

adult

The important line is:

const message = if (age >= 18) "adult" else "minor";

If the condition is true, the whole if expression becomes:

"adult"

If the condition is false, it becomes:

"minor"

Then that value is stored in message.

This is useful when you want to choose a value, not run a large block of code.

Both Branches Must Match

When if returns a value, both branches must produce compatible types.

This is valid:

const n = 10;
const label = if (n >= 0) "positive" else "negative";

Both branches produce string literals.

This is invalid:

const n = 10;
const value = if (n >= 0) 123 else "negative";

One branch gives an integer. The other branch gives a string. Zig will reject this because value needs one type.

Zig wants the type of each expression to be clear.

Blocks Can Return Values

An if expression can use blocks too.

When a block should produce a value, use break with the block value.

const std = @import("std");

pub fn main() void {
    const score = 75;

    const result = if (score >= 60) blk: {
        std.debug.print("checking pass branch\n", .{});
        break :blk "pass";
    } else blk: {
        std.debug.print("checking fail branch\n", .{});
        break :blk "fail";
    };

    std.debug.print("result: {s}\n", .{result});
}

This program prints:

checking pass branch
result: pass

The label blk names the block. The line:

break :blk "pass";

means: leave this block and use "pass" as the block’s value.

For beginners, this syntax may look unusual. You do not need it for simple if statements. But it matters later because Zig treats blocks as expressions.

if with Optionals

Zig has optional values. An optional value may contain something, or it may contain nothing.

The “nothing” value is null.

Example:

const maybe_number: ?u32 = 42;

The type ?u32 means: either a u32, or null.

You can use if to unwrap an optional:

const std = @import("std");

pub fn main() void {
    const maybe_number: ?u32 = 42;

    if (maybe_number) |number| {
        std.debug.print("number: {}\n", .{number});
    } else {
        std.debug.print("no number\n", .{});
    }
}

This prints:

number: 42

This part:

if (maybe_number) |number| {

means: if maybe_number contains a value, take the value out and call it number.

Inside the block, number is a normal u32.

If maybe_number is null, the else block runs.

const maybe_number: ?u32 = null;

if (maybe_number) |number| {
    std.debug.print("number: {}\n", .{number});
} else {
    std.debug.print("no number\n", .{});
}

This prints:

no number

This is one of the most common uses of if in Zig.

if with Error Unions

Zig also uses if with error unions.

An error union is a value that may be either a successful value or an error.

For example:

fn parseNumber() !u32 {
    return 123;
}

The return type !u32 means: this function returns either a u32 or an error.

You can handle that with if:

const std = @import("std");

fn parseNumber() !u32 {
    return 123;
}

pub fn main() void {
    if (parseNumber()) |number| {
        std.debug.print("number: {}\n", .{number});
    } else |err| {
        std.debug.print("error: {}\n", .{err});
    }
}

If parseNumber() succeeds, the success value is assigned to number.

If it fails, the error is assigned to err.

This style makes success and failure visible in the code.

When to Use if

Use if when your program needs to choose between paths.

Use a plain if when there is only one special case.

if (count == 0) {
    std.debug.print("empty\n", .{});
}

Use if and else when there are two paths.

if (count == 0) {
    std.debug.print("empty\n", .{});
} else {
    std.debug.print("not empty\n", .{});
}

Use else if when there are several ordered tests.

if (score >= 90) {
    std.debug.print("A\n", .{});
} else if (score >= 80) {
    std.debug.print("B\n", .{});
} else {
    std.debug.print("lower\n", .{});
}

Use switch when you are comparing one value against many exact cases. We will cover switch in the next section.

The Main Idea

if lets a Zig program make a choice.

The condition must be a boolean. The true branch runs when the condition is true. The else branch runs when the condition is false.

Zig also lets if produce a value, unwrap optionals, and handle error unions.

The beginner rule is simple: write the condition clearly, keep each branch small, and make every choice visible in the code.