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:
zigfn 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:
zigconst array_size = comptime fibonacci(10); // Computed at compile time var buffer: [array_size]u8 = undefined;
- •
Type introspection and generation:
zigfn 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 T - •
anyerror!Tallows any error type
Error handling patterns:
- •
try- propagate errors:zigconst contents = try readFile("data.txt"); // If error, returns immediately from current function - •
catch- handle errors:zigconst contents = readFile("data.txt") catch |err| { std.debug.print("Error: {}\n", .{err}); return; }; - •
catchwith default value:zigconst contents = readFile("data.txt") catch "default content"; - •
errdefer- cleanup on error:zigfn 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:zigif (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:zigconst value = maybe_value orelse 0; // Use 0 if null
- •
orelse- early return:zigconst value = maybe_value orelse return; // Return if null
- •
.?- assert non-null (unsafe):zigconst 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 patterns - •
references/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.