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.
?*TRead this as:
optional pointer to TFor example:
?*i32means:
either a pointer to an i32, or nullWhy 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:
?*i32That 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:
*i32An optional pointer can be null:
?*i32This difference is visible in the type.
That means when you see this:
fn update(value: *i32) voidyou know value is expected to point to a valid i32.
When you see this:
fn updateMaybe(value: ?*i32) voidyou 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; // errorThis 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; // errorWhy?
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 = 20The 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 valueThis 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 TFor example:
?*const i32means:
either a pointer to a read-only i32, or nullExample:
fn findFirst(values: []const i32, target: i32) ?*const i32 {
for (values) |*value| {
if (value.* == target) {
return value;
}
}
return null;
}The function receives:
[]const i32So it cannot modify the array.
It returns:
?*const i32So 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 = 30Use ?*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: ?*Nodemeans:
this node may point to another node, or it may be the last nodeExample:
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
30The 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:
?*TIt means:
maybe points to TIt does not mean:
owns TFor 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 nullExample 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 u8can represent both:
some bytes
zero bytesAn 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 providedand:
a slice was provided, but it is emptyThe 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; // errorYou 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) voidIf draw cannot do anything useful without an image, then the parameter should be:
fn draw(image: *Image) voidNow 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 absentBefore 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.