Zig Optional Types
An optional type represents a value that might exist or might not. Every programming problem eventually involves "no result" scenarios — a search that finds nothing, a setting that has not been configured, a user input that was not provided. Zig models these situations with optional types, which force you to handle the "no value" case before accessing the data.
The Optional Concept
Regular type: Optional type:
+---------+ +-------------------+
| i32 | | ?i32 |
| Always | | May have a value |
| has a | | or may be null |
| value | +-------------------+
+---------+ | |
x = 42 | |
has value is null
(some 42) (nothing)
A question mark before the type makes it optional. i32 always holds an integer. ?i32 holds either an integer or null.
Declaring Optional Variables
var score: ?i32 = null; // no score yet score = 88; // now it has a value score = null; // back to no value
const name: ?[]const u8 = null; // no name const found: ?usize = 42; // index found const result: ?f64 = compute_value(); // depends on function
Checking and Unwrapping an Optional
Using if-capture
const maybe_age: ?u8 = 25;
if (maybe_age) |age| {
std.debug.print("Age is {d}\n", .{age});
} else {
std.debug.print("Age unknown\n", .{});
}
maybe_age = ?u8
|
Has a value?
|
yes → capture age = 25 → print "Age is 25"
no → print "Age unknown"
Using orelse — Default Value
The orelse operator provides a fallback value when the optional is null:
const discount: ?u32 = null;
const applied = discount orelse 0;
std.debug.print("Discount: {d}%\n", .{applied}); // 0
discount = null
|
orelse 0
|
applied = 0
const saved_name: ?[]const u8 = "Pradeep";
const display = saved_name orelse "Guest";
std.debug.print("Hello, {s}!\n", .{display}); // Hello, Pradeep!
Using orelse with a Block
The orelse block can execute multiple statements before returning the fallback:
const value = expensive_lookup() orelse blk: {
std.debug.print("Computing default...\n", .{});
break :blk 42;
};
Forceful Unwrap — Use With Caution
const x: ?i32 = 100;
const n = x.?; // panic if x is null!
std.debug.print("{d}\n", .{n}); // 100
The .? suffix forces the optional open. If the value is null, the program panics at runtime. Use this only when you are absolutely certain the value exists — prefer the if-capture pattern in most situations.
While Loop with Optionals
A while loop can iterate as long as an optional has a value:
fn nextItem(idx: *usize) ?u32 {
const data = [_]u32{ 10, 30, 50, 70 };
if (idx.* >= data.len) return null;
const val = data[idx.*];
idx.* += 1;
return val;
}
var i: usize = 0;
while (nextItem(&i)) |item| {
std.debug.print("Item: {d}\n", .{item});
}
nextItem returns: → 10 (i=0) → 30 (i=1) → 50 (i=2) → 70 (i=3) → null (i=4) → loop ends Output: Item: 10 Item: 30 Item: 50 Item: 70
Optional Pointers
Regular pointer: *i32 → MUST point somewhere valid
Optional pointer: ?*i32 → MAY be null
In C:
int *p = NULL; ← no type protection. Can dereference accidentally.
In Zig:
var p: ?*i32 = null; ← type is explicit.
p.*; ← COMPILE ERROR. Must unwrap first.
if (p) |ptr| {
ptr.*; ← safe dereference
}
Optional pointers eliminate null dereference bugs at the type level. In Zig, you cannot accidentally dereference a possibly-null pointer — the compiler requires you to check first.
Chaining Optionals with orelse
fn findUser(id: u32) ?[]const u8 {
if (id == 1) return "Alice";
return null;
}
fn getUserCity(name: []const u8) ?[]const u8 {
if (std.mem.eql(u8, name, "Alice")) return "Mumbai";
return null;
}
const user_id: u32 = 1;
const city = blk: {
const name = findUser(user_id) orelse break :blk "Unknown city";
break :blk getUserCity(name) orelse "City not found";
};
std.debug.print("City: {s}\n", .{city});
Optional in Structs
const UserProfile = struct {
username: []const u8,
email: []const u8,
phone: ?[]const u8 = null, // phone is optional
bio: ?[]const u8 = null, // bio is optional
};
const user = UserProfile{
.username = "zigfan99",
.email = "fan@example.com",
.phone = "9876543210",
// bio left as null
};
if (user.bio) |bio| {
std.debug.print("Bio: {s}\n", .{bio});
} else {
std.debug.print("No bio provided.\n", .{});
}
Optional vs Error Union
Use optional (?T) when: Use error union (!T) when: +------------------------+ +---------------------------+ | "No result" is normal | | Failure needs explanation | | Search returns nothing | | File read fails | | Setting not configured | | Network error occurred | | User typed nothing | | Invalid input format | +------------------------+ +---------------------------+ ?[]const u8 → found the name or didn't find it ![]const u8 → read the file or failed with an error message
Choose optional when absence of a value is a normal, expected outcome. Choose an error union when something went wrong and you need to communicate why. Mixing these up leads to code that either swallows errors silently or over-engineers simple lookups.
