Optional Pointers
An optional pointer is a pointer that may have no value.
Optional Pointers
An optional pointer is a pointer that may have no value.
In Zig, a normal pointer must point to something valid. It cannot be null.
var x: i32 = 10;
const p: *i32 = &x;
Here, p points to x.
This is not allowed:
const p: *i32 = null;
A normal pointer says:
I point to a valid i32.
But sometimes a pointer needs to represent absence. For that, Zig uses an optional pointer.
?*T
Read this as:
optional pointer to T
For example:
?*i32
means:
either a pointer to an i32, or null
Why Optional Pointers Exist
Many programs need to express “found” or “not found.”
Suppose we search for a number in a small array. If we find it, we want to return a pointer to the matching item. If we do not find it, we need to return “nothing.”
That is exactly what an optional pointer represents.
fn findFirst(values: []i32, target: i32) ?*i32 {
for (values) |*value| {
if (value.* == target) {
return value;
}
}
return null;
}
The return type is:
?*i32
That means the function may return a pointer to an i32, or it may return null.
The caller must check before using the pointer.
Null Is Explicit
In some languages, every pointer or object reference can be null. This causes many runtime errors because any access may fail unexpectedly.
Zig takes a stricter approach.
A normal pointer cannot be null:
*i32
An optional pointer can be null:
?*i32
This difference is visible in the type.
That means when you see this:
fn update(value: *i32) void
you know value is expected to point to a valid i32.
When you see this:
fn updateMaybe(value: ?*i32) void
you know the function must handle the possibility of null.
The type tells the truth.
Creating an Optional Pointer
You can assign a real pointer to an optional pointer:
var x: i32 = 10;
const maybe: ?*i32 = &x;
You can also assign null:
const maybe: ?*i32 = null;
Both are valid because maybe is optional.
A non-optional pointer cannot hold null.
var x: i32 = 10;
const good: ?*i32 = &x;
const empty: ?*i32 = null;
// const bad: *i32 = null; // error
This is the basic rule:
Use *T when the pointer must exist.
Use ?*T when the pointer may be absent.
Unwrapping an Optional Pointer
You cannot directly dereference an optional pointer.
This is not valid:
const maybe: ?*i32 = &x;
// maybe.* = 20; // error
Why?
Because maybe might be null.
You must unwrap it first.
The most common way is if:
if (maybe) |p| {
p.* = 20;
}
Inside the if block, p is a normal pointer.
If maybe contains a pointer, the block runs.
If maybe is null, the block does not run.
Full example:
const std = @import("std");
pub fn main() void {
var x: i32 = 10;
const maybe: ?*i32 = &x;
if (maybe) |p| {
p.* = 20;
}
std.debug.print("x = {}\n", .{x});
}
Output:
x = 20
The optional pointer is checked before use.
Handling the Null Case
Often you need to handle both cases.
if (maybe) |p| {
std.debug.print("value = {}\n", .{p.*});
} else {
std.debug.print("no value\n", .{});
}
Complete program:
const std = @import("std");
pub fn main() void {
const maybe: ?*i32 = null;
if (maybe) |p| {
std.debug.print("value = {}\n", .{p.*});
} else {
std.debug.print("no value\n", .{});
}
}
Output:
no value
This is one of Zig’s strengths. The null case is not hidden. The code must say what happens.
Optional Pointer Search Example
Here is a complete search example:
const std = @import("std");
fn findFirst(values: []i32, target: i32) ?*i32 {
for (values) |*value| {
if (value.* == target) {
return value;
}
}
return null;
}
pub fn main() void {
var numbers = [_]i32{ 10, 20, 30, 40 };
const result = findFirst(numbers[0..], 30);
if (result) |p| {
p.* = 99;
}
std.debug.print("{any}\n", .{numbers});
}
Output:
{ 10, 20, 99, 40 }
The function returns a pointer to the matching element.
The caller modifies the element through that pointer.
Because the pointer points into the original array, the array changes.
Optional Pointer to Const
An optional pointer can also point to read-only data.
?*const T
For example:
?*const i32
means:
either a pointer to a read-only i32, or null
Example:
fn findFirst(values: []const i32, target: i32) ?*const i32 {
for (values) |*value| {
if (value.* == target) {
return value;
}
}
return null;
}
The function receives:
[]const i32
So it cannot modify the array.
It returns:
?*const i32
So the caller can read the found value, but cannot modify it.
const std = @import("std");
fn findFirst(values: []const i32, target: i32) ?*const i32 {
for (values) |*value| {
if (value.* == target) {
return value;
}
}
return null;
}
pub fn main() void {
const numbers = [_]i32{ 10, 20, 30, 40 };
const result = findFirst(numbers[0..], 30);
if (result) |p| {
std.debug.print("found = {}\n", .{p.*});
}
}
Output:
found = 30
Use ?*const T when the pointer may be missing and the value should only be read.
Use ?*T when the pointer may be missing and the value may be modified.
Optional Pointer as a Field
Optional pointers are common in data structures.
For example, a linked list node may point to the next node. The last node has no next node.
const Node = struct {
value: i32,
next: ?*Node,
};
The field:
next: ?*Node
means:
this node may point to another node, or it may be the last node
Example:
const std = @import("std");
const Node = struct {
value: i32,
next: ?*Node,
};
pub fn main() void {
var third = Node{ .value = 30, .next = null };
var second = Node{ .value = 20, .next = &third };
var first = Node{ .value = 10, .next = &second };
var current: ?*Node = &first;
while (current) |node| {
std.debug.print("{}\n", .{node.value});
current = node.next;
}
}
Output:
10
20
30
The loop continues while current contains a pointer. It stops when current becomes null.
This is a natural use of optional pointers.
Optional Pointers and Function Parameters
A function parameter should use a normal pointer when the pointer is required.
fn reset(value: *i32) void {
value.* = 0;
}
This function should not accept null. It needs a real i32.
Use an optional pointer only when absence is meaningful.
fn resetMaybe(value: ?*i32) void {
if (value) |p| {
p.* = 0;
}
}
This function says:
If a value is provided, reset it. If not, do nothing.
Do not use optional pointers just because they seem flexible. They make every caller and every function body deal with null.
A stricter type is usually better.
Optional Pointers and Ownership
An optional pointer does not own memory.
This is still only a reference:
?*T
It means:
maybe points to T
It does not mean:
owns T
For example:
var x: i32 = 10;
const maybe: ?*i32 = &x;
The optional pointer points to x, but it does not own x. It must not outlive x.
The same lifetime rule applies:
A pointer is valid only while the memory it points to is valid.
Optionality does not change that.
Optional Pointer vs Optional Value
These two types are different:
?i32
?*i32
?i32 means:
either an i32 value, or null
?*i32 means:
either a pointer to an i32 somewhere else, or null
Example with optional value:
fn maybeNumber(ok: bool) ?i32 {
if (ok) return 42;
return null;
}
This returns the number itself.
Example with optional pointer:
fn maybePointer(ok: bool, value: *i32) ?*i32 {
if (ok) return value;
return null;
}
This returns a pointer to a number owned elsewhere.
Use an optional value when you want to return data directly.
Use an optional pointer when you want to refer to existing data.
Optional Pointer vs Empty Slice
Sometimes beginners use null when an empty slice would be better.
For sequences, prefer an empty slice when “no items” is a valid sequence.
[]const u8
can represent both:
some bytes
zero bytes
An empty slice has length 0.
const empty = bytes[0..0];
Use an optional slice only when there is a real difference between:
no slice was provided
and:
a slice was provided, but it is empty
The same idea applies to pointers.
Use null only when absence means something.
Optional Pointer Syntax Summary
| Syntax | Meaning |
|---|---|
*T |
pointer to mutable T, cannot be null |
*const T |
pointer to read-only T, cannot be null |
?*T |
pointer to mutable T, or null |
?*const T |
pointer to read-only T, or null |
null |
no value |
if (maybe) |p| |
unwrap optional pointer |
Common Mistake: Dereferencing Before Checking
This is wrong:
const maybe: ?*i32 = null;
// maybe.* = 10; // error
You must check first:
if (maybe) |p| {
p.* = 10;
}
This is not just syntax. It is a safety rule.
The program must prove that the pointer exists before using it.
Common Mistake: Using Optional Pointers Too Often
Optional pointers are useful, but they should not be the default.
This is weak API design:
fn draw(image: ?*Image) void
If draw cannot do anything useful without an image, then the parameter should be:
fn draw(image: *Image) void
Now the caller must provide a real image.
Use optional pointers only when null is a real, expected state.
The Main Idea
A normal pointer must point to a valid value.
An optional pointer may point to a valid value, or it may be null.
This is the difference:
*T // must exist
?*T // may be absent
Before using an optional pointer, unwrap it with if, orelse, or another optional-handling form.
Optional pointers make absence explicit. They help you write APIs where null is visible in the type instead of hidden as a runtime surprise.