This skill should be used when the user asks questions like "how does comptime work in Zig", "explain Zig error handling", "what are Zig optionals", "how do I use defer in Zig", "what are Zig's basic...
This skill provides essential Zig language concepts for experienced programmers transitioning from other languages. It focuses on Zig's distinctive features: comptime (compile-time execution), error handling, optionals, and memory control primitives.
Target Zig version: 0.15.2
Comptime is Zig's most distinctive feature, enabling code execution during compilation.
What it does:
Common use cases:
Generic functions and types:
fn max(comptime T: type, a: T, b: T) T {
return if (a > b) a else b;
}
The comptime T: type means T is determined at compile time, creating monomorphized versions.
Comptime computation:
const array_size = comptime fibonacci(10); // Computed at compile time
var buffer: [array_size]u8 = undefined;
Type introspection and generation:
fn printFields(comptime T: type) void {
inline for (@typeInfo(T).Struct.fields) |field| {
std.debug.print("{s}\n", .{field.name});
}
}
Key points:
comptime keywordcomptime must be known at compile timeinline for to unroll loops at compile time@typeInfo(T)For more detailed patterns, see references/comptime-patterns.md.
Zig uses explicit error handling without exceptions. Errors are values, not control flow.
Error unions:
const FileError = error{ FileNotFound, PermissionDenied };
fn readFile(path: []const u8) FileError![]u8 {
// Returns either error or []u8
if (file_not_exist) return error.FileNotFound;
return file_contents;
}
The ! operator:
!T means "error union" - either an error or value of type Tanyerror!T allows any error typeError handling patterns:
try - propagate errors:
const contents = try readFile("data.txt");
// If error, returns immediately from current function
catch - handle errors:
const contents = readFile("data.txt") catch |err| {
std.debug.print("Error: {}\n", .{err});
return;
};
catch with default value:
const contents = readFile("data.txt") catch "default content";
errdefer - cleanup on error:
fn allocateResource() !*Resource {
const resource = try allocate();
errdefer deallocate(resource); // Only runs if function returns error
try initialize(resource);
return resource;
}
Key principles:
try is syntactic sugar for catch |err| return errOptionals represent values that may or may not exist, replacing null pointers safely.
Syntax:
const maybe_value: ?i32 = null; // Optional i32
const has_value: ?i32 = 42; // Contains value
Checking and unwrapping:
if unwrapping:
if (maybe_value) |value| {
// value is i32 here, not ?i32
std.debug.print("Value: {}\n", .{value});
} else {
std.debug.print("No value\n", .{});
}
orelse - provide default:
const value = maybe_value orelse 0; // Use 0 if null
orelse - early return:
const value = maybe_value orelse return; // Return if null
.? - assert non-null (unsafe):
const value = maybe_value.?; // Panics if null
Combining with errors:
fn findUser(id: u32) !?User {
// Can return error OR null OR User
if (database_error) return error.DatabaseDown;
if (user_not_found) return null;
return user;
}
// Usage:
const user = try findUser(123) orelse return error.UserNotFound;
Key distinction from other languages:
null only exists for optional types (?T)defer executes code when scope exits, regardless of how it exits (return, error, etc.).
Basic usage:
fn processFile(path: []const u8) !void {
const file = try std.fs.cwd().openFile(path, .{});
defer file.close(); // Guaranteed to run when function exits
// Work with file...
// file.close() automatically called
}
Multiple defers execute in reverse order (LIFO):
defer std.debug.print("Third\n", .{});
defer std.debug.print("Second\n", .{});
defer std.debug.print("First\n", .{});
// Prints: First, Second, Third
errdefer - cleanup only on error:
fn createWidget() !*Widget {
const widget = try allocator.create(Widget);
errdefer allocator.destroy(widget); // Only if function errors
try widget.initialize(); // If this fails, widget is destroyed
return widget;
}
Key patterns:
defer immediately after acquiring resourceerrdefer for partial initialization cleanupInteger types:
i8, u8, i16, u16, i32, u32, i64, u64, i128, u128isize, usize (pointer-sized integers)i7, u24, etc. (any bit count)Float types:
f16, f32, f64, f80, f128Boolean:
bool - only true or falseArrays and slices:
const array: [5]i32 = .{ 1, 2, 3, 4, 5 }; // Fixed-size array
const slice: []const i32 = &array; // Slice references array
Struct initialization:
const Point = struct {
x: f32,
y: f32,
};
const point = Point{ .x = 1.0, .y = 2.0 };
Anonymous struct literals:
const point = .{ .x = 1.0, .y = 2.0 }; // Type inferred
Undefined and initialization:
undefined - uninitialized memory (performance optimization)if is an expression:
const value = if (condition) 42 else 0;
switch is exhaustive:
const value: i32 = 2;
const result = switch (value) {
1 => "one",
2 => "two",
3, 4, 5 => "three to five",
else => "other",
};
while with continue expressions:
var i: usize = 0;
while (i < 10) : (i += 1) { // Continue expression runs each iteration
// Loop body
}
for for arrays and slices:
for (items) |item| {
std.debug.print("{}\n", .{item});
}
// With index:
for (items, 0..) |item, i| {
std.debug.print("{}: {}\n", .{ i, item });
}
When examining Zig code:
comptime - indicates compile-time logic! return errors?T types may be nullcomptime params vs runtime paramsResult type (error + optional):
fn operation() !?Value {
// Returns: error, null, or Value
}
Allocator parameter:
fn createThing(allocator: std.mem.Allocator) !Thing {
const data = try allocator.alloc(u8, size);
// allocator passed explicitly, no hidden allocation
}
Payload capture:
if (maybe_value) |value| {
// Use value
}
if (result) |value| {
// Success case
} else |err| {
// Error case
}
From C/C++:
@intCast, etc.)From Rust:
From Go/Java/Python:
For detailed examples and advanced patterns:
references/comptime-patterns.md - Advanced comptime techniques, type introspection, generic patternsreferences/error-patterns.md - Comprehensive error handling patterns, error sets, custom errors| Concept | Syntax | Purpose |
|---|---|---|
| Comptime parameter | comptime T: type |
Generic/compile-time value |
| Error union | !T |
Function may return error or T |
| Optional | ?T |
Value may be null |
| Unwrap optional | value orelse default |
Provide default if null |
| Try error | try expr |
Propagate error or unwrap value |
| Catch error | expr catch handler |
Handle error case |
| Defer cleanup | defer expr |
Execute when scope exits |
| Error defer | errdefer expr |
Execute only on error |
| Type introspection | @typeInfo(T) |
Get compile-time type info |
When working with Zig code, focus on understanding the explicit flow: where errors originate, how memory is allocated, and what executes at compile time versus runtime.