⚡ Zig Guide LiveUnofficial
✓ Zig 0.17.0-dev.1454+5faa79730On an older Zig?

Optionals

A ?T holds either a T or null. The compiler will not let you use the value without handling the null case first, so there is no such thing as an accidental null dereference.

These are real tests. Run executes the actual zig test runner, compiled to WebAssembly.

const std = @import("std");
const expect = std.testing.expect;

test "optional payload capture" {
    var maybe: ?i32 = null;
    try expect(maybe == null);

    maybe = 42;
    if (maybe) |value| {
        try expect(value == 42);
    } else {
        unreachable;
    }
}

test "orelse supplies a default" {
    const nothing: ?u32 = null;
    try expect((nothing orelse 7) == 7);
}

test "optional pointers are free" {
    // A `?*T` is the same size as a `*T`; null is the 0 address.
    try expect(@sizeOf(?*i32) == @sizeOf(*i32));
}

Payload capture

The if (maybe) |value| form is payload capture: inside the block, value is a plain i32, not an optional. The unwrapping and the null check are the same piece of syntax, so they cannot drift apart.

orelse is the shorthand when all you want is a fallback:

const port = maybe_port orelse 8080;

Optional pointers are free

?*T is the same size as *T. Zig uses the null address as the null tag, so optional pointers cost nothing: no extra byte, no wrapper struct. The last test above asserts exactly this with @sizeOf.

Note that this only applies to pointers. A ?u32 genuinely needs a tag byte alongside the payload, because every u32 bit pattern is a valid u32.