
zig-fundamentals
This skill should be used when the user asks questions like "how does comptime work in Zig", "explai
Zig Fundamentals
Overview
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
Core Concepts
Comptime: Compile-Time Execution
Comptime is Zig's most distinctive feature, enabling code execution during compilation.
What it does:
- Executes arbitrary Zig code at compile time
- Generates code, types, and data structures before runtime
- Eliminates need for macros, preprocessor, or code generation tools
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: typemeans 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:
- Any expression can be forced to compile-time with
comptimekeyword - Parameters marked
comptimemust be known at compile time - Use
inline forto unroll loops at compile time - Access type information with
@typeInfo(T)
For more detailed patterns, see references/comptime-patterns.md.
Error Handling
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:
!Tmeans "error union" - either an error or value of type Tanyerror!Tallows any error type
Error 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; }; -
catchwith 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:
- Errors are compile-time known sets of values
- No hidden control flow (no exceptions)
tryis syntactic sugar forcatch |err| return err- Always handle or propagate errors explicitly
Optionals
Optionals 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:
-
ifunwrapping: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:
nullonly exists for optional types (?T)- Regular pointers cannot be null in Zig
- Optional pointers have same size as regular pointers (null is optimized)
Defer: Guaranteed Cleanup
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:
- Use
deferimmediately after acquiring resource errdeferfor partial initialization cleanup- Defers execute in reverse order of declaration
- Scope-based, not function-based (works in blocks)
Basic Types and Values
Integer types:
- Explicit bit sizes:
i8,u8,i16,u16,i32,u32,i64,u64,i128,u128 - Platform-dependent:
isize,usize(pointer-sized integers) - Arbitrary bit width:
i7,u24, etc. (any bit count)
Float types:
f16,f32,f64,f80,f128
Boolean:
bool- onlytrueorfalse- No implicit integer conversion
Arrays 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)- Use for arrays/buffers that will be filled immediately
- Debug builds catch usage of undefined values
Control Flow Differences
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 });
}
Working with Zig Code
Reading Zig Code
When examining Zig code:
- Look for
comptime- indicates compile-time logic - Check error types - functions with
!return errors - Identify optionals -
?Ttypes may be null - Find defers - locate resource cleanup
- Read parameter types -
comptimeparams vs runtime params
Common Patterns
Result 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
}
Transitioning from Other Languages
From C/C++:
- No implicit casts - use explicit conversion (
@intCast, etc.) - No undefined behavior in safe mode (checked in Debug/ReleaseSafe)
- No hidden control flow (no exceptions, no operator overloading)
- Comptime replaces templates/macros
From Rust:
- No borrow checker - manual memory management with allocators
- Explicit allocator passing instead of implicit lifetimes
- Error unions instead of Result<T, E> enum
- Optionals instead of Option<T> enum
From Go/Java/Python:
- No garbage collector - manual memory management
- Explicit error handling, no exceptions
- No hidden allocations - all memory explicit
- Value types by default, not references
Additional Resources
Reference Files
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
Official Documentation
Quick Reference
| 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 |
Tips for Success
- Embrace explicit error handling - no hidden exceptions improves reliability
- Use comptime for generics - replaces C++ templates with readable code
- Leverage defer for cleanup - simpler than try/finally or RAII
- Pass allocators explicitly - makes memory allocation visible
- Read the standard library - idiomatic patterns and common utilities
- Use Debug build mode - catches undefined behavior and safety violations
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.