Optionals & Null Safety
?T says clearly: this value is either a real T or null. The compiler then refuses to use the value until you unwrap it — the null-check that C forgets becomes a syntax requirement in Zig.
Optional Basics
An optional is not a pointer hack — it is its own type. A variable of type ?u32 holds either a u32 payload or the special value null.
The ?T Type
const maybe: ?u32 = 42; // an optional holding a value
const nothing: ?u32 = null; // an optional holding nothing
The null Keyword
null is the only value of the null type and it coerces into any optional. There is no hidden null in non-optional types: a plain u32 is always a number, a plain *T always points somewhere.
Unwrapping
An optional inside a computation is a dead end until unwrapped. Zig offers four visible idioms; each names its intent.
orelse — Provide a Fallback
const a: ?u32 = null;
const safe = a orelse 0; // 0 when null, otherwise the payload
std.debug.print("{d}\n", .{safe});
if Capture
if (a) |value| {
// value is the unwrapped payload — guaranteed non-null here
std.debug.print("got {d}\n", .{value});
} else {
std.debug.print("was null\n", .{});
}
while Capture
Iterating over a nullable stream is the strongest pattern: each loop iteration unwraps one value, and the loop ends cleanly at the first null.
var node: ?*Node = head;
while (node) |n| : (node = n.next) {
std.debug.print("{s}\n", .{n.name});
}
The .? Operator
.? asserts non-null: if the optional is null the program panics in safe builds. Use it only where you have proven the value exists — it is the explicit version of a guaranteed-unwrap.
const b: ?u32 = 5;
const forced = b.?; // 5 — panics if b were null
Figure 1 — an optional carries a payload and a present flag; every unwrap method checks the flag.
Optional Pointers
For pointers, the optional degenerates to a null pointer at runtime — zero extra memory. That is why C code that uses NULL everywhere maps so naturally to ?*T in Zig, with the check made mandatory at compile time.
?*T
fn find(hay: []const u8, needle: u8) ?usize {
for (hay, 0..) |byte, index| {
if (byte == needle) return index;
}
return null; // absence is a first-class result
}
const pos = find("sage", g) orelse 0;
Coercions
A plain value coerces into the optional automatically — const x: ?u32 = 5; needs no ceremony. The reverse never happens implicitly: unwrapping is always written down.
Representation and Semantics
Under the hood, an optional is a payload plus one bit of presence, typically a tag; for pointers the null address serves as the tag. When the payload type has spare bit patterns (like some enums and pointers), Zig reuses them so the optional costs nothing.
Layout
// ?*const T is a single machine word — a possibly-null address
// ?u32 is u32 plus a present bit (two words on common targets)
const opt_size = @sizeOf(?u32);
Common Pitfalls
Unwrap on Null
Both .? and an if-capture on a null optional crash loudly in safe builds. The crash is correct — it means the guarantee was wrong — but prefer orelse or an explicit if when null is a realistic outcome.
Nested Optionals
??u32 (an optional of an optional) is allowed but almost always a design smell. Flatten the intent: either the outer level is meaningful or it is not.
Next: Errors & Error Unions — failures as typed values.