🎯

zig-best-practices

🎯Skill

from 0xbigboss/claude-code

VibeIndex|
What it does

Enforces type-first development in Zig by leveraging tagged unions, explicit error sets, comptime validation, and type safety techniques.

zig-best-practices

Installation

Install skill:
npx skills add https://github.com/0xbigboss/claude-code --skill zig-best-practices
38
AddedJan 27, 2026

Skill Details

SKILL.md

Provides Zig patterns for type-first development with tagged unions, explicit error sets, comptime validation, and memory management. Must use when reading or writing Zig files.

Overview

# Zig Best Practices

Type-First Development

Types define the contract before implementation. Follow this workflow:

  1. Define data structures - structs, unions, and error sets first
  2. Define function signatures - parameters, return types, and error unions
  3. Implement to satisfy types - let the compiler guide completeness
  4. Validate at comptime - catch invalid configurations during compilation

Make Illegal States Unrepresentable

Use Zig's type system to prevent invalid states at compile time.

Tagged unions for mutually exclusive states:

```zig

// Good: only valid combinations possible

const RequestState = union(enum) {

idle,

loading,

success: []const u8,

failure: anyerror,

};

fn handleState(state: RequestState) void {

switch (state) {

.idle => {},

.loading => showSpinner(),

.success => |data| render(data),

.failure => |err| showError(err),

}

}

// Bad: allows invalid combinations

const RequestState = struct {

loading: bool,

data: ?[]const u8,

err: ?anyerror,

};

```

Explicit error sets for failure modes:

```zig

// Good: documents exactly what can fail

const ParseError = error{

InvalidSyntax,

UnexpectedToken,

EndOfInput,

};

fn parse(input: []const u8) ParseError!Ast {

// implementation

}

// Bad: anyerror hides failure modes

fn parse(input: []const u8) anyerror!Ast {

// implementation

}

```

Distinct types for domain concepts:

```zig

// Prevent mixing up IDs of different types

const UserId = enum(u64) { _ };

const OrderId = enum(u64) { _ };

fn getUser(id: UserId) !User {

// Compiler prevents passing OrderId here

}

fn createUserId(raw: u64) UserId {

return @enumFromInt(raw);

}

```

Comptime validation for invariants:

```zig

fn Buffer(comptime size: usize) type {

if (size == 0) {

@compileError("buffer size must be greater than 0");

}

if (size > 1024 * 1024) {

@compileError("buffer size exceeds 1MB limit");

}

return struct {

data: [size]u8 = undefined,

len: usize = 0,

};

}

```

Non-exhaustive enums for extensibility:

```zig

// External enum that may gain variants

const Status = enum(u8) {

active = 1,

inactive = 2,

pending = 3,

_,

};

fn processStatus(status: Status) !void {

switch (status) {

.active => {},

.inactive => {},

.pending => {},

_ => return error.UnknownStatus,

}

}

```

Module Structure

Larger cohesive files are idiomatic in Zig. Keep related code together: tests alongside implementation, comptime generics at file scope, public/private controlled by pub. Split only when a file handles genuinely separate concerns. The standard library demonstrates this pattern with files like std/mem.zig containing 2000+ lines of cohesive memory operations.

Instructions

  • Return errors with context using error unions (!T); every function returns a value or an error. Explicit error sets document failure modes.
  • Use errdefer for cleanup on error paths; use defer for unconditional cleanup. This prevents resource leaks without try-finally boilerplate.
  • Handle all branches in switch statements; include an else clause that returns an error or uses unreachable for truly impossible cases.
  • Pass allocators explicitly to functions requiring dynamic memory; prefer std.testing.allocator in tests for leak detection.
  • Prefer const over var; prefer slices over raw pointers for bounds safety. Immutability signals intent and enables optimizations.
  • Avoid anytype; prefer explicit comptime T: type parameters. Explicit types document intent and produce clearer error messages.
  • Use std.log.scoped for namespaced logging; define a module-level log constant for consistent scope across the file.
  • Add or update tests for new logic; use std.testing.allocator to catch memory leaks automatically.

Examples

Explicit failure for unimplemented logic:

```zig

fn buildWidget(widget_type: []const u8) !Widget {

return error.NotImplemented;

}

```

Propagate errors with try:

```zig

fn readConfig(path: []const u8) !Config {

const file = try std.fs.cwd().openFile(path, .{});

defer file.close();

const contents = try file.readToEndAlloc(allocator, max_size);

return parseConfig(contents);

}

```

Resource cleanup with errdefer:

```zig

fn createResource(allocator: std.mem.Allocator) !*Resource {

const resource = try allocator.create(Resource);

errdefer allocator.destroy(resource);

resource.* = try initializeResource();

return resource;

}

```

Exhaustive switch with explicit default:

```zig

fn processStatus(status: Status) ![]const u8 {

return switch (status) {

.active => "processing",

.inactive => "skipped",

_ => error.UnhandledStatus,

};

}

```

Testing with memory leak detection:

```zig

const std = @import("std");

test "widget creation" {

const allocator = std.testing.allocator;

var list: std.ArrayListUnmanaged(u32) = .empty;

defer list.deinit(allocator);

try list.append(allocator, 42);

try std.testing.expectEqual(1, list.items.len);

}

```

Memory Management

  • Pass allocators explicitly; never use global state for allocation. Functions declare their allocation needs in parameters.
  • Use defer immediately after acquiring a resource. Place cleanup logic next to acquisition for clarity.
  • Prefer arena allocators for temporary allocations; they free everything at once when the arena is destroyed.
  • Use std.testing.allocator in tests; it reports leaks with stack traces showing allocation origins.

Examples

Allocator as explicit parameter:

```zig

fn processData(allocator: std.mem.Allocator, input: []const u8) ![]u8 {

const result = try allocator.alloc(u8, input.len * 2);

errdefer allocator.free(result);

// process input into result

return result;

}

```

Arena allocator for batch operations:

```zig

fn processBatch(items: []const Item) !void {

var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);

defer arena.deinit();

const allocator = arena.allocator();

for (items) |item| {

const processed = try processItem(allocator, item);

try outputResult(processed);

}

// All allocations freed when arena deinits

}

```

Logging

  • Use std.log.scoped to create namespaced loggers; each module should define its own scoped logger for filtering.
  • Define a module-level const log at the top of the file; use it consistently throughout the module.
  • Use appropriate log levels: err for failures, warn for suspicious conditions, info for state changes, debug for tracing.

Examples

Scoped logger for a module:

```zig

const std = @import("std");

const log = std.log.scoped(.widgets);

pub fn createWidget(name: []const u8) !Widget {

log.debug("creating widget: {s}", .{name});

const widget = try allocateWidget(name);

log.debug("created widget id={d}", .{widget.id});

return widget;

}

pub fn deleteWidget(id: u32) void {

log.info("deleting widget id={d}", .{id});

// cleanup

}

```

Multiple scopes in a codebase:

```zig

// In src/db.zig

const log = std.log.scoped(.db);

// In src/http.zig

const log = std.log.scoped(.http);

// In src/auth.zig

const log = std.log.scoped(.auth);

```

Comptime Patterns

  • Use comptime parameters for generic functions; type information is available at compile time with zero runtime cost.
  • Prefer compile-time validation over runtime checks when possible. Catch errors during compilation rather than in production.
  • Use @compileError for invalid configurations that should fail the build.

Examples

Generic function with comptime type:

```zig

fn max(comptime T: type, a: T, b: T) T {

return if (a > b) a else b;

}

```

Compile-time validation:

```zig

fn createBuffer(comptime size: usize) [size]u8 {

if (size == 0) {

@compileError("buffer size must be greater than 0");

}

return [_]u8{0} ** size;

}

```

Avoiding anytype

  • Prefer comptime T: type over anytype; explicit type parameters document expected constraints and produce clearer errors.
  • Use anytype only when the function genuinely accepts any type (like std.debug.print) or for callbacks/closures.
  • When using anytype, add a doc comment describing the expected interface or constraints.

Examples

Prefer explicit comptime type (good):

```zig

fn sum(comptime T: type, items: []const T) T {

var total: T = 0;

for (items) |item| {

total += item;

}

return total;

}

```

Avoid anytype when type is known (bad):

```zig

// Unclear what types are valid; error messages will be confusing

fn sum(items: anytype) @TypeOf(items[0]) {

// ...

}

```

Acceptable anytype for callbacks:

```zig

/// Calls callback for each item. Callback must accept (T) and return void.

fn forEach(comptime T: type, items: []const T, callback: anytype) void {

for (items) |item| {

callback(item);

}

}

```

Using @TypeOf when anytype is necessary:

```zig

fn debugPrint(value: anytype) void {

const T = @TypeOf(value);

if (@typeInfo(T) == .Pointer) {

std.debug.print("ptr: {*}\n", .{value});

} else {

std.debug.print("val: {}\n", .{value});

}

}

```

Error Handling Patterns

  • Define specific error sets for functions; avoid anyerror when possible. Specific errors document failure modes.
  • Use catch with a block for error recovery or logging; use catch unreachable only when errors are truly impossible.
  • Merge error sets with || when combining operations that can fail in different ways.

Examples

Specific error set:

```zig

const ConfigError = error{

FileNotFound,

ParseError,

InvalidFormat,

};

fn loadConfig(path: []const u8) ConfigError!Config {

// implementation

}

```

Error handling with catch block:

```zig

const value = operation() catch |err| {

std.log.err("operation failed: {}", .{err});

return error.OperationFailed;

};

```

Configuration

  • Load config from environment variables at startup; validate required values before use. Missing config should cause a clean exit with a descriptive message.
  • Define a Config struct as single source of truth; avoid std.posix.getenv scattered throughout code.
  • Use sensible defaults for development; require explicit values for production secrets.

Examples

Typed config struct:

```zig

const std = @import("std");

pub const Config = struct {

port: u16,

database_url: []const u8,

api_key: []const u8,

env: []const u8,

};

pub fn loadConfig() !Config {

const db_url = std.posix.getenv("DATABASE_URL") orelse

return error.MissingDatabaseUrl;

const api_key = std.posix.getenv("API_KEY") orelse

return error.MissingApiKey;

const port_str = std.posix.getenv("PORT") orelse "3000";

const port = std.fmt.parseInt(u16, port_str, 10) catch

return error.InvalidPort;

return .{

.port = port,

.database_url = db_url,

.api_key = api_key,

.env = std.posix.getenv("ENV") orelse "development",

};

}

```

Optionals

  • Use orelse to provide default values for optionals; use .? only when null is a program error.
  • Prefer if (optional) |value| pattern for safe unwrapping with access to the value.

Examples

Safe optional handling:

```zig

fn findWidget(id: u32) ?*Widget {

// lookup implementation

}

fn processWidget(id: u32) !void {

const widget = findWidget(id) orelse return error.WidgetNotFound;

try widget.process();

}

```

Optional with if unwrapping:

```zig

if (maybeValue) |value| {

try processValue(value);

} else {

std.log.warn("no value present", .{});

}

```

Advanced Topics

Reference these guides for specialized patterns:

  • Building custom containers (queues, stacks, trees): See [GENERICS.md](GENERICS.md)
  • Interfacing with C libraries (raylib, SDL, curl, system APIs): See [C-INTEROP.md](C-INTEROP.md)
  • Debugging memory leaks (GPA, stack traces): See [DEBUGGING.md](DEBUGGING.md)

References

  • Language Reference: https://ziglang.org/documentation/0.15.2/
  • Standard Library: https://ziglang.org/documentation/0.15.2/std/
  • Code Samples: https://ziglang.org/learn/samples/
  • Zig Guide: https://zig.guide/

More from this repository10

🎯
react-best-practices🎯Skill

Guides React developers in writing clean, efficient components by providing best practices for hooks, effects, and component design.

🎯
python-best-practices🎯Skill

Guides Python developers in implementing type-first development with robust type checking, dataclasses, discriminated unions, and domain-specific type primitives.

🎯
openai-image-gen🎯Skill

Generates images using OpenAI's image generation API within the Claude Code environment.

🎯
tamagui-best-practices🎯Skill

Optimizes Tamagui configurations and styling patterns for high-performance, cross-platform React Native and web applications with v4 compiler settings.

🎯
playwright-best-practices🎯Skill

Provides Playwright test best practices for creating resilient, maintainable tests using user-facing locators, Page Object Models, and robust testing patterns.

🎯
go-best-practices🎯Skill

Enforces type-first Go development with custom types, interfaces, and patterns to create robust, self-documenting code with strong compile-time guarantees.

🎯
web-fetch🎯Skill

Fetches web content and converts HTML to clean markdown using intelligent content extraction and CSS selectors.

🎯
typescript-best-practices🎯Skill

Enforces TypeScript best practices by guiding developers to create robust, type-safe code through compile-time validation, discriminated unions, and explicit type definitions.

🎯
gen-env🎯Skill

Generates environment configuration files or environment variables for development projects, ensuring consistent and reproducible setup across different environments.

🎯
zig-docs🎯Skill

Retrieves Zig language documentation from Codeberg sources, standard library files, and online references via CLI commands.