Overview
Teams stay nimble when their naming, comments, and module layout follow a predictable rhythm. This appendix distills the house style into a quick reference that you can keep open while reviewing pull requests or scaffolding new modules.
Zig 0.15.2 tightened formatter output, stabilized doc comment handling, and clarified testing ergonomics; adopting those defaults means less time negotiating conventions and more time verifying behavior.v0.15.2
Learning Goals
- Audit a module quickly by scanning for the canonical ordering of doc comments, types, functions, and tests.
- Describe what “tight error vocabulary” means in Zig and when to prefer bespoke error sets over
anyerror. - Wire deterministic tests alongside the code they document without sacrificing readability in larger files.
_Refs: _
Voice & Naming at a Glance
Readable code starts with alignment between prose and identifiers: the doc comment should speak the same nouns that the exported symbol implements, while helper functions keep verbs short and active.fmt.zig Following this pattern lets reviewers focus on semantics instead of debating word choice.
Naming, Comments, and Writers
This example pairs module-level narration with focused doc comments and uses a fixed buffer writer so the tests never touch the allocator.Io.zig
//! Demonstrates naming and documentation conventions for a small diagnostic helper.
const std = @import("std");
/// Represents a labelled temperature reading captured during diagnostics.
pub const TemperatureReading = struct {
label: []const u8,
value_celsius: f32,
/// Writes the reading to the provided writer using canonical casing and units.
pub fn format(self: TemperatureReading, writer: anytype) !void {
try writer.print("{s}: {d:.1}°C", .{ self.label, self.value_celsius });
}
};
/// Creates a reading with the given label and temperature value in Celsius.
pub fn createReading(label: []const u8, value_celsius: f32) TemperatureReading {
return .{
.label = label,
.value_celsius = value_celsius,
};
}
test "temperature readings print with consistent label casing" {
const reading = createReading("CPU", 72.25);
var backing: [64]u8 = undefined;
var stream = std.io.fixedBufferStream(&backing);
try reading.format(stream.writer());
const rendered = stream.getWritten();
try std.testing.expectEqualStrings("CPU: 72.3°C", rendered);
}
$ zig test 01_naming_and_comments.zigAll 1 tests passed.Formatting the descriptive sentence before the code encourages readers to skim the type signature and the test together; keeping terminology aligned with the doc comment mirrors the advice in Chapter 36.36
Tight Error Vocabularies
Precise error sets balance empathy for callers with lightweight control flow; instead of returning anyerror, we list exactly the states the parser can reach and promote them to public API surface.math.zig
//! Keeps error vocabulary tight for a numeric parser so callers can react precisely.
const std = @import("std");
/// Enumerates the failure modes that the parser can surface to its callers.
pub const ParseCountError = error{
EmptyInput,
InvalidDigit,
Overflow,
};
/// Parses a decimal counter while preserving descriptive error information.
pub fn parseCount(input: []const u8) ParseCountError!u32 {
if (input.len == 0) return ParseCountError.EmptyInput;
var acc: u64 = 0;
for (input) |char| {
if (char < '0' or char > '9') return ParseCountError.InvalidDigit;
const digit: u64 = @intCast(char - '0');
acc = acc * 10 + digit;
if (acc > std.math.maxInt(u32)) return ParseCountError.Overflow;
}
return @intCast(acc);
}
test "parseCount reports invalid digits precisely" {
try std.testing.expectEqual(@as(u32, 42), try parseCount("42"));
try std.testing.expectError(ParseCountError.InvalidDigit, parseCount("4a"));
try std.testing.expectError(ParseCountError.EmptyInput, parseCount(""));
try std.testing.expectError(ParseCountError.Overflow, parseCount("42949672960"));
}
$ zig test 02_error_vocabulary.zigAll 1 tests passed.The test suite demonstrates that each branch stays reachable, preventing dead strings and teaching consumers which names to switch on without reading the implementation.36
Module Layout Checklist
When a file exports configuration helpers, keep the public façade first, collect private validators underneath, and end with table-driven tests that read as documentation.12
//! Highlights a layered module layout with focused helper functions and tests.
const std = @import("std");
/// Errors that can emerge while normalizing user-provided retry policies.
pub const RetryPolicyError = error{
ZeroAttempts,
ExcessiveDelay,
};
/// Encapsulates retry behaviour for a network client, including sensible defaults.
pub const RetryPolicy = struct {
max_attempts: u8 = 3,
delay_ms: u32 = 100,
/// Indicates whether exponential backoff is active.
pub fn isBackoffEnabled(self: RetryPolicy) bool {
return self.delay_ms > 0 and self.max_attempts > 1;
}
};
/// Partial options provided by configuration files or CLI flags.
pub const PartialRetryOptions = struct {
max_attempts: ?u8 = null,
delay_ms: ?u32 = null,
};
/// Builds a retry policy from optional overrides while keeping default reasoning centralized.
pub fn makeRetryPolicy(options: PartialRetryOptions) RetryPolicy {
return RetryPolicy{
.max_attempts = options.max_attempts orelse 3,
.delay_ms = options.delay_ms orelse 100,
};
}
fn validate(policy: RetryPolicy) RetryPolicyError!RetryPolicy {
if (policy.max_attempts == 0) return RetryPolicyError.ZeroAttempts;
if (policy.delay_ms > 60_000) return RetryPolicyError.ExcessiveDelay;
return policy;
}
/// Produces a validated policy, emphasising the flow from raw input to constrained output.
pub fn finalizeRetryPolicy(options: PartialRetryOptions) RetryPolicyError!RetryPolicy {
const policy = makeRetryPolicy(options);
return validate(policy);
}
test "finalize rejects zero attempts" {
try std.testing.expectError(
RetryPolicyError.ZeroAttempts,
finalizeRetryPolicy(.{ .max_attempts = 0 }),
);
}
test "finalize accepts defaults" {
const policy = try finalizeRetryPolicy(.{});
try std.testing.expectEqual(@as(u8, 3), policy.max_attempts);
try std.testing.expect(policy.isBackoffEnabled());
}
$ zig test 03_module_layout.zigAll 2 tests passed.Locating the error set at the top keeps the type graph obvious and mirrors how std.testing materializes invariants right next to the code that depends on them.testing.zig
Patterns to Keep on Hand
- Reserve
//!for module-level narration and///for API documentation so generated references keep a consistent voice across packages.36 - Pair every exposed helper with a focused test block; Zig’s test runner makes colocated tests free, and they double as executable usage examples.
- When the formatter reflows signatures, accept its judgment—consistency between editors and CI was one of the major quality-of-life improvements in 0.15.x.
Notes & Caveats
- Do not suppress warnings from
zig fmt; instead, adjust the code so the defaults succeed and document any unavoidable divergence in your contributing guide.36 - Keep project-local lint scripts in sync with upstream Zig releases so chore churn stays low during toolchain upgrades.
- If your API emits container types from
std, reference their exact field names in doc comments—callers can jump tozig/lib/stddirectly to confirm semantics.hash_map.zig
Exercises
- Rewrite one of your recent modules by grouping constants, types, functions, and tests in the order shown above, then run
zig fmtto confirm the structure stays stable.36 - Extend
parseCountto accept underscores for readability while maintaining a strict error vocabulary; add targeted tests for the new branch. - Generate HTML documentation for a project using
zig build docand review how//!and///comments surface—tune the prose until the output reads smoothly.
Alternatives & Edge Cases
- Some teams prefer fully separated test files; if you do, adopt the same naming and doc comment patterns so search results stay predictable.36
- For modules that expose comptime-heavy APIs, include a
test "comptime"block so these guidelines still deliver runnable coverage.15 - When vendoring third-party code, annotate deviations from this style in a short README so reviewers know the divergence is intentional.Build.zig