Skip to content

Optional Types

An optional type is a type that can hold either a value or no value.

An optional type is a type that can hold either a value or no value.

In Zig, “no value” is written as null.

For example:

const maybe_number: ?i32 = 10;

The type ?i32 means:

either an i32 value, or null

So this variable can hold an integer:

const a: ?i32 = 123;

Or it can hold no integer:

const b: ?i32 = null;

The ? is part of the type. It says: this value may be missing.

Why Optional Types Exist

Many programs need to represent “maybe there is a value.”

For example:

A search function may find an item, or it may find nothing.

A parser may read a number, or the input may not contain a number.

A pointer may point to something, or it may point to nothing.

A configuration field may be provided, or it may be absent.

Without optional types, programmers often use special values:

// Bad style for this case
// -1 means "not found"
const index: i32 = -1;

This works, but it is fragile. The meaning of -1 is only a convention. The type system does not know that -1 means “missing.”

With an optional type, the meaning is explicit:

const index: ?usize = null;

Now the type itself says that the value may be absent.

The Shape of an Optional Type

An optional type is written like this:

?T

where T is the real type inside.

Examples:

?i32
?usize
?bool
?[]const u8
?*User

Read them like this:

?i32         maybe an i32
?usize       maybe a usize
?bool        maybe a bool
?[]const u8  maybe a string slice
?*User       maybe a pointer to User

The optional type wraps another type.

So this:

const name: ?[]const u8 = "Zig";

means:

name is either a string slice, or null

A Simple Example

Suppose we want a function that finds the first even number in a list.

Sometimes the list has an even number. Sometimes it does not.

const std = @import("std");

fn firstEven(numbers: []const i32) ?i32 {
    for (numbers) |n| {
        if (@mod(n, 2) == 0) {
            return n;
        }
    }

    return null;
}

pub fn main() void {
    const values = [_]i32{ 3, 7, 9, 12, 15 };

    const result = firstEven(values[0..]);

    if (result) |value| {
        std.debug.print("first even number: {}\n", .{value});
    } else {
        std.debug.print("no even number found\n", .{});
    }
}

The function returns ?i32.

fn firstEven(numbers: []const i32) ?i32

That return type says:

This function may return an i32.
It may also return null.

Inside the function, we return a normal integer when we find one:

return n;

If we do not find one, we return null:

return null;

Checking an Optional Value

You cannot use an optional value exactly like the value inside it.

This is not allowed:

const maybe_number: ?i32 = 10;
const doubled = maybe_number * 2; // error

Why? Because maybe_number might be null.

Before using the inner i32, you must unwrap the optional.

The most common way is if:

const maybe_number: ?i32 = 10;

if (maybe_number) |number| {
    const doubled = number * 2;
    _ = doubled;
}

Inside this block, number is not optional. It is a plain i32.

This syntax:

if (maybe_number) |number| {
    // use number here
}

means:

If maybe_number contains a value, call that value number and run this block.

You can also use else:

const maybe_number: ?i32 = null;

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

Optional Values Make Absence Explicit

Optional types are useful because they force you to handle the missing case.

Consider this function:

fn findUser(id: u64) ?User {
    // returns a User if found
    // returns null if not found
}

The caller cannot pretend that the user always exists. The return type says otherwise.

The caller must write code like this:

if (findUser(42)) |user| {
    // use user
} else {
    // handle missing user
}

This makes the program clearer.

A function that returns User promises that it always returns a user.

A function that returns ?User says that the user may be missing.

Those are different promises.

Optional Is Not an Error

An optional value means “there may be no value.”

It does not mean “something went wrong.”

For example, searching a list and finding nothing is usually not an error. It is a normal result.

fn indexOf(items: []const u8, target: u8) ?usize {
    for (items, 0..) |item, i| {
        if (item == target) {
            return i;
        }
    }

    return null;
}

If the target is absent, returning null is reasonable.

But if a file cannot be opened, that is different. There may be many reasons: permission denied, file not found, disk failure, invalid path. That should usually be an error union, not a plain optional.

fn openFile(path: []const u8) !File {
    // may fail with an error
}

Use an optional when absence is an ordinary possibility.

Use an error when something failed and the caller may need to know why.

Optional Pointers

Optional pointers are common in Zig.

const maybe_ptr: ?*i32 = null;

This means:

maybe a pointer to i32, or null

This is different from a normal pointer:

const ptr: *i32 = undefined;

A normal *i32 cannot be null. If a pointer may be missing, write it as optional:

?*i32

This is an important Zig rule. Nullability is explicit.

In C, many pointer types can be null. In Zig, a pointer is non-null unless the type says otherwise.

That makes pointer code easier to reason about.

Optional Fields in Structs

Optional types are often used in structs.

const User = struct {
    id: u64,
    name: []const u8,
    email: ?[]const u8,
};

Here, every user has an id and a name.

But email is optional.

const alice = User{
    .id = 1,
    .name = "Alice",
    .email = "[email protected]",
};

const bob = User{
    .id = 2,
    .name = "Bob",
    .email = null,
};

This is clearer than using an empty string to mean “no email.”

.email = ""

An empty string is still a string. It may mean “the email is empty.” It may mean “unknown.” It may mean “not provided.” The type does not say.

With ?[]const u8, the meaning is explicit.

Optional Values and Defaults

Sometimes you want to use a default value when an optional is null.

Zig provides the orelse operator:

const maybe_port: ?u16 = null;
const port = maybe_port orelse 8080;

This means:

If maybe_port has a value, use it.
Otherwise, use 8080.

Another example:

const maybe_name: ?[]const u8 = null;
const name = maybe_name orelse "guest";

Now name is a normal []const u8, not an optional.

Optional Types Are Small but Important

Optional types look simple, but they change how you design APIs.

Compare these two functions:

fn getName() []const u8

and:

fn getName() ?[]const u8

The first function promises that a name always exists.

The second function says that a name may be absent.

That small ? changes the contract.

Good Zig code uses optional types when absence is part of the normal model. This keeps the program honest. The type tells the reader what can happen, and the compiler makes sure the code handles it.