4 Language Idioms & Core Patterns
- Naming:
snake_case(vars/functions),PascalCase(types),SCREAMING_SNAKE_CASE(constants) - Cleanup:
defer cleanup()(runs at scope exit in LIFO order),errdefer(only on error paths) - Errors:
!Tfor error unions,trypropagates,catchhandles, see Ch7 for details - Optionals:
?Tfor nullable values,.?unwraps or panics,orelseprovides default - comptime: Compile-time execution for generics and zero-cost abstractions
- Jump to: Naming §1.2 | defer §1.3 | comptime §1.5
This chapter establishes the idiomatic baseline for Zig development. These patterns form the foundation for all subsequent chapters, covering naming conventions, resource cleanup, error handling fundamentals, compile-time execution, and module organization. Most patterns work identically across Zig 0.14.0, 0.14.1, 0.15.1, and 0.15.2.
4.1 Overview
Zig’s language design prioritizes explicitness, simplicity, and maintainability. Unlike languages with implicit behaviors (garbage collection, hidden allocations, exceptions), Zig makes costs and control flow visible in code. This chapter teaches the patterns that define idiomatic Zig:
- Naming conventions communicate intent through consistent case rules
- defer and errdefer provide deterministic resource cleanup
- Error unions and optionals handle failure and absence explicitly
- comptime enables zero-cost abstractions through compile-time execution
- Module organization structures code for clarity and maintainability
These idioms reflect community consensus drawn from the official style guide, production codebases (TigerBeetle, Ghostty, Bun), and ecosystem libraries.1
4.2 Core Concepts
Naming Conventions
Zig uses three case styles with specific semantic meanings:2
PascalCase — Types (structs, enums, unions, opaques)
camelCase — Functions returning values
Exception: Functions returning types use PascalCase:
snake_case — Variables, parameters, constants, and zero-field structs (namespaces)
File names typically use snake_case, except when a file directly exposes a single type (e.g., ArrayList.zig).3
Real-World Patterns
TigerBeetle’s style guide adds precision through systematic naming:4
Units and qualifiers in descending order of significance:
Acronym capitalization preserves readability:
Domain-meaningful names convey ownership and lifecycle:
defer and errdefer
Zig’s defer executes code when leaving the current scope (via return, break, or block end). errdefer executes only when leaving via error return.5 See Ch7 for comprehensive coverage of resource cleanup patterns.
Execution order is LIFO:
const std = @import("std");
fn demonstrateDeferOrder() void {
defer std.debug.print("3. Third (executed first)\n", .{});
defer std.debug.print("2. Second\n", .{});
defer std.debug.print("1. First (executed last)\n", .{});
std.debug.print("0. Function body\n", .{});
}
// Output:
// 0. Function body
// 1. First (executed last)
// 2. Second
// 3. Third (executed first)Resource cleanup pairs acquisition with deferred release:
errdefer handles partial failures:
const std = @import("std");
fn createResources(allocator: std.mem.Allocator) !struct { a: []u8, b: []u8 } {
const a = try allocator.alloc(u8, 100);
errdefer allocator.free(a); // Only frees if subsequent operations fail
const b = try allocator.alloc(u8, 200);
errdefer allocator.free(b);
return .{ .a = a, .b = b };
}If the second allocation fails, errdefer ensures the first allocation is cleaned up before returning the error.
Error Unions vs Optionals
Zig distinguishes between failure (!T) and absence (?T):6
Error unions (!T) represent operations that can fail:
Optionals (?T) represent values that may not exist:
Decision criteria: - Use !T when absence indicates a problem requiring error handling - Use ?T when absence is a valid, expected state - Use !?T when an operation can fail or return nothing (e.g., optional database query with possible connection error)
Handling patterns:
// Error union with try (propagates error)
const value = try parseNumber("42");
// Error union with catch (handles error)
const value = parseNumber("42") catch 0;
// Optional with orelse (provides default)
const index = findFirst(items, 10) orelse return;
// Optional with if (conditional execution)
if (findFirst(items, 10)) |index| {
// Use index
}comptime Fundamentals
Zig’s comptime keyword forces evaluation at compile time, enabling zero-cost generics and type manipulation.7
Generic functions use compile-time type parameters:
The function is instantiated separately for each type at compile time. No runtime overhead.
Compile-time validation catches errors before execution:
Type introspection with @typeInfo:
Module Organization
Zig files are modules. The @import builtin loads other modules and the standard library.8
Basic imports:
Visibility control with pub:
File organization patterns:9
Flat structure (small projects):
src/
├── main.zig
├── parser.zig
└── renderer.zig
Hierarchical (medium projects):
src/
├── main.zig
├── core/
│ ├── types.zig
│ └── errors.zig
└── utils/
├── io.zig
└── strings.zig
Module-as-directory (large projects):
src/
├── main.zig
└── parser/
├── parser.zig // Re-exports public API
├── lexer.zig
└── ast.zig
Example parser/parser.zig:
Clients import: const parser = @import("parser/parser.zig");
4.3 Code Examples
Example 1: Combining defer with Error Handling
const std = @import("std");
fn copyFile(
allocator: std.mem.Allocator,
src_path: []const u8,
dst_path: []const u8
) !void {
const src = try std.fs.cwd().openFile(src_path, .{});
defer src.close();
const dst = try std.fs.cwd().createFile(dst_path, .{});
defer dst.close();
const buffer = try allocator.alloc(u8, 4096);
defer allocator.free(buffer);
while (true) {
const bytes_read = try src.read(buffer);
if (bytes_read == 0) break;
try dst.writeAll(buffer[0..bytes_read]);
}
}Each resource is cleaned up in reverse order of acquisition, even if a later operation fails.
Example 2: Error Union with Optional
const std = @import("std");
fn findUser(db: *Database, id: u32) !?User {
const conn = try db.connect(); // Can fail
defer conn.close();
return conn.query("SELECT * FROM users WHERE id = ?", .{id}) catch |err| {
if (err == error.NotFound) return null; // Absence is valid
return err; // Other errors propagate
};
}This pattern distinguishes connection failures (errors) from missing users (null).
Example 3: Generic Data Structure
const std = @import("std");
fn Stack(comptime T: type) type {
return struct {
items: std.ArrayList(T),
const Self = @This();
pub fn init(allocator: std.mem.Allocator) Self {
return .{ .items = std.ArrayList(T).init(allocator) };
}
pub fn deinit(self: *Self) void {
self.items.deinit();
}
pub fn push(self: *Self, item: T) !void {
try self.items.append(item);
}
pub fn pop(self: *Self) ?T {
return self.items.popOrNull();
}
};
}
test "generic stack" {
const allocator = std.testing.allocator;
var stack = Stack(i32).init(allocator);
defer stack.deinit();
try stack.push(10);
try stack.push(20);
try std.testing.expect(stack.pop().? == 20);
try std.testing.expect(stack.pop().? == 10);
try std.testing.expect(stack.pop() == null);
}Example 4: Module with Re-exports
// shapes.zig
pub const Point = struct {
x: i32,
y: i32,
pub fn add(self: Point, other: Point) Point {
return .{ .x = self.x + other.x, .y = self.y + other.y };
}
};
pub const Rectangle = struct {
top_left: Point,
width: u32,
height: u32,
};
pub fn distance(a: Point, b: Point) f64 {
const dx = @as(f64, @floatFromInt(a.x - b.x));
const dy = @as(f64, @floatFromInt(a.y - b.y));
return @sqrt(dx * dx + dy * dy);
}
// main.zig
const shapes = @import("shapes.zig");
pub fn main() void {
const p1 = shapes.Point{ .x = 0, .y = 0 };
const p2 = shapes.Point{ .x = 3, .y = 4 };
const dist = shapes.distance(p1, p2);
}4.4 Common Pitfalls
Pitfall 1: defer in Loops
Problem: defer in a loop executes once per iteration, accumulating deferred statements:
Solution: Use a nested block:
Pitfall 2: Using Optionals for Error States
Problem: Optionals cannot explain why something is missing:
Solution: Use error unions:
Pitfall 3: comptime Type Confusion
Problem: Mixing compile-time and runtime values without proper annotations:
Solution: Mark type parameters as comptime:
Pitfall 4: Using Explicit Re-exports
❌ Incorrect — usingnamespace (removed):
✅ Correct — Explicit re-exports:
The usingnamespace keyword was removed to improve clarity around public API boundaries.10
4.5 In Practice
TigerBeetle: Safety-First Patterns
TigerBeetle mandates minimum 2 assertions per function and comprehensive error handling:11
View source: vsr.zig
Ghostty: Conditional Compilation
Ghostty uses comptime to select platform-specific entry points:12
View source: main.zig
Bun: Advanced comptime
Bun uses comptime string maps for zero-cost lookups:13
View source: comptime_string_map.zig
4.6 Summary
This chapter established the idiomatic baseline for Zig development:
Naming conventions use three case styles with semantic meaning: PascalCase for types, camelCase for functions, snake_case for variables. Production codebases extend these rules with domain-specific qualifiers and unit suffixes.
defer and errdefer provide deterministic cleanup in LIFO order. Pair resource acquisition with deferred cleanup immediately. Use errdefer for partial-failure rollback. Avoid defer in loops without nested blocks.
Error unions (!T) represent operations that can fail. Optionals (?T) represent values that may not exist. Choose based on whether absence indicates a problem (error union) or a valid state (optional). Combine as !?T when both failure and absence are possible.
comptime enables zero-cost abstractions through compile-time execution. Use it for generic functions, compile-time validation, and type introspection. All type parameters must be marked comptime.
Module organization uses @import for code reuse and pub for visibility control. Structure projects flat (small), hierarchical (medium), or module-as-directory (large). Explicitly re-export public APIs rather than using usingnamespace (removed in 0.15).
These patterns remain stable across Zig 0.14.0, 0.14.1, 0.15.1, and 0.15.2, with the notable exception of usingnamespace removal. Later chapters build on these foundations for memory management, I/O, concurrency, and build systems.