Skip to content

Appendix I. Zig Coding Style Guide

Zig code should be explicit, simple, and easy to inspect. The goal is not cleverness. The goal is code that another programmer can read, verify, and maintain.

Zig code should be explicit, simple, and easy to inspect. The goal is not cleverness. The goal is code that another programmer can read, verify, and maintain.

I.1 Prefer const by Default

Use const unless the value must change.

const x = 10;

Use var only when mutation is required.

var count: usize = 0;
count += 1;

This makes the code easier to reason about. When a value is const, the reader knows it will not be reassigned.

I.2 Give Types When They Help

Zig can infer many types.

const x = 10;

But explicit types help when the exact size matters.

const port: u16 = 8080;
const count: usize = items.len;

Use explicit types for public APIs, integer widths, file formats, protocols, and data structures.

I.3 Keep Functions Small

A good Zig function should do one clear job.

Bad:

fn run() !void {
    // parse args
    // open files
    // allocate buffers
    // process data
    // write output
}

Better:

fn run() !void {
    const config = try parseArgs();
    const input = try readInput(config);
    const result = try process(input);
    try writeOutput(result);
}

Small functions make error paths, ownership, and cleanup easier to see.

I.4 Put Cleanup Near Acquisition

Use defer immediately after acquiring a resource.

const file = try std.fs.cwd().openFile("data.txt", .{});
defer file.close();

For allocated memory:

const buffer = try allocator.alloc(u8, 1024);
defer allocator.free(buffer);

This reduces leaks and makes ownership visible.

I.5 Use errdefer for Partial Construction

When a function builds an object in stages, use errdefer.

fn createUser(allocator: std.mem.Allocator, name: []const u8) !User {
    const owned_name = try allocator.dupe(u8, name);
    errdefer allocator.free(owned_name);

    return User{
        .name = owned_name,
    };
}

If the function fails before returning, errdefer cleans up.

I.6 Pass Allocators Explicitly

A function that may allocate should usually take an allocator.

fn readName(allocator: std.mem.Allocator) ![]u8 {
    return try allocator.dupe(u8, "zig");
}

This tells the caller that memory ownership is involved.

Avoid hidden global allocation.

I.7 Document Ownership

When returning allocated memory, say who frees it.

/// Returns newly allocated memory.
/// Caller owns the returned slice and must free it with `allocator.free`.
fn copyText(allocator: std.mem.Allocator, text: []const u8) ![]u8 {
    return try allocator.dupe(u8, text);
}

When returning borrowed memory, say how long it lives.

/// Returns a slice pointing into `input`.
/// The returned slice must not outlive `input`.
fn firstWord(input: []const u8) []const u8 {
    // ...
}

I.8 Prefer Slices for Buffers

Use slices when passing many items.

fn sum(values: []const i32) i32 {
    var total: i32 = 0;

    for (values) |value| {
        total += value;
    }

    return total;
}

A slice carries both pointer and length. That is usually safer than a raw pointer plus a separate length.

I.9 Avoid Long-Lived Borrowed Slices

Do not store a slice unless you know the memory behind it will stay alive.

Risky:

const User = struct {
    name: []const u8,
};

This is safe only if name points to memory that outlives the User.

For long-lived data, copy the bytes:

user.name = try allocator.dupe(u8, input_name);

I.10 Keep Error Handling Visible

Do not hide errors unnecessarily.

Use try when the current function should return the error.

try saveFile(path, data);

Use catch when this function can handle it.

saveFile(path, data) catch |err| {
    std.log.err("failed to save file: {}", .{err});
    return err;
};

Avoid empty catches unless ignoring the error is truly safe.

doSomething() catch {};

That code should be rare.

I.11 Use Specific Errors

Prefer meaningful error names.

const ConfigError = error{
    MissingName,
    InvalidPort,
    UnknownOption,
};

This is better than returning a vague failure everywhere.

I.12 Avoid Clever comptime

comptime is powerful, but it can make code harder to read.

Good use:

fn max(comptime T: type, a: T, b: T) T {
    return if (a > b) a else b;
}

Risky use:

// large compile-time code generator
// hard to debug
// hard to understand

Use compile-time programming when it removes repetition or enforces correctness. Avoid it when ordinary runtime code is clearer.

I.13 Use Clear Names

Prefer names that explain meaning.

Good:

const request_count = 10;
const input_path = "data.txt";
const allocator = arena.allocator();

Weak:

const x = 10;
const s = "data.txt";
const a = arena.allocator();

Short names are fine in small scopes:

for (items) |item| {
    // item is clear here
}

I.14 Keep Public APIs Boring

Public APIs should be predictable.

Good:

pub fn parseConfig(allocator: std.mem.Allocator, text: []const u8) !Config

This signature shows:

The function may allocate.

The function reads text.

The function may fail.

The function returns Config.

Avoid APIs where allocation, failure, or ownership is hidden.

I.15 Prefer Plain Data

Zig works well with plain structs.

const Point = struct {
    x: f64,
    y: f64,
};

Add methods when they improve readability.

const Point = struct {
    x: f64,
    y: f64,

    fn lengthSquared(self: Point) f64 {
        return self.x * self.x + self.y * self.y;
    }
};

Do not force object-oriented patterns where plain data is enough.

I.16 Use Tagged Unions for Variants

When a value can be one of several shapes, use union(enum).

const Token = union(enum) {
    number: i64,
    identifier: []const u8,
    plus,
    minus,
};

Then handle it with switch.

switch (token) {
    .number => |n| std.debug.print("{}\n", .{n}),
    .identifier => |name| std.debug.print("{s}\n", .{name}),
    .plus => {},
    .minus => {},
}

This is safer than manually tracking a tag and payload separately.

I.17 Avoid Global Mutable State

Global mutable state makes programs harder to test and reason about.

Prefer passing state explicitly.

fn run(config: Config, allocator: std.mem.Allocator) !void {
    // ...
}

Instead of hiding dependencies in globals.

I.18 Keep Imports Simple

Put imports near the top.

const std = @import("std");
const parser = @import("parser.zig");

Avoid deeply nested import tricks. A file should make its dependencies easy to see.

I.19 Use Tests Near the Code

Zig lets you place tests in the same file.

fn add(a: i32, b: i32) i32 {
    return a + b;
}

test "add returns the sum" {
    try std.testing.expectEqual(@as(i32, 5), add(2, 3));
}

Small tests close to the code help future changes.

I.20 Format Code Consistently

Use:

zig fmt .

Zig has a standard formatter. Use it.

Do not hand-format code into a personal style that fights the formatter.

I.21 Check Return Values

If a function returns a value, either use it or explicitly discard it.

_ = c.printf("hello\n");

The discard shows intent.

Do not leave unused values by accident.

I.22 Write Comments for Why, Not Noise

Bad comment:

// increment i
i += 1;

Good comment:

// Keep one byte for the trailing zero required by the C API.
const usable_len = buffer.len - 1;

Comments should explain decisions, invariants, ownership, or non-obvious constraints.

I.23 Prefer Simple Control Flow

Deep nesting makes error paths hard to follow.

Harder:

if (valid) {
    if (ready) {
        if (allowed) {
            try run();
        }
    }
}

Clearer:

if (!valid) return error.Invalid;
if (!ready) return error.NotReady;
if (!allowed) return error.NotAllowed;

try run();

Early returns are often clearer in Zig.

I.24 Keep Unsafe Code Small

Pointer casts, manual alignment, C interop, volatile memory, and packed layouts need care.

Put risky code behind a small API.

fn readHeader(bytes: []const u8) !Header {
    // unsafe or low-level details stay here
}

Then the rest of the program uses the safe wrapper.

I.25 Style Rule Summary

Good Zig style means:

PracticeReason
Prefer constReduces accidental mutation
Pass allocatorsMakes allocation visible
Use deferKeeps cleanup reliable
Use slicesKeeps pointer plus length together
Handle errors explicitlyMakes failure visible
Keep functions smallMakes ownership and control flow clearer
Use zig fmtKeeps style consistent
Avoid hidden globalsImproves testing and reasoning
Document ownershipPrevents memory bugs

Zig style follows Zig’s main idea: make the important parts of the program visible.