zig-best-practices
π―Skillfrom 0xbigboss/claude-code
Enforces type-first development in Zig by leveraging tagged unions, explicit error sets, comptime validation, and type safety techniques.
Installation
npx skills add https://github.com/0xbigboss/claude-code --skill zig-best-practicesSkill Details
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:
- Define data structures - structs, unions, and error sets first
- Define function signatures - parameters, return types, and error unions
- Implement to satisfy types - let the compiler guide completeness
- 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
errdeferfor cleanup on error paths; usedeferfor unconditional cleanup. This prevents resource leaks without try-finally boilerplate. - Handle all branches in
switchstatements; include anelseclause that returns an error or usesunreachablefor truly impossible cases. - Pass allocators explicitly to functions requiring dynamic memory; prefer
std.testing.allocatorin tests for leak detection. - Prefer
constovervar; prefer slices over raw pointers for bounds safety. Immutability signals intent and enables optimizations. - Avoid
anytype; prefer explicitcomptime T: typeparameters. Explicit types document intent and produce clearer error messages. - Use
std.log.scopedfor namespaced logging; define a module-levellogconstant for consistent scope across the file. - Add or update tests for new logic; use
std.testing.allocatorto 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
deferimmediately 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.allocatorin 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.scopedto create namespaced loggers; each module should define its own scoped logger for filtering. - Define a module-level
const logat the top of the file; use it consistently throughout the module. - Use appropriate log levels:
errfor failures,warnfor suspicious conditions,infofor state changes,debugfor 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
comptimeparameters 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
@compileErrorfor 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: typeoveranytype; explicit type parameters document expected constraints and produce clearer errors. - Use
anytypeonly when the function genuinely accepts any type (likestd.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
anyerrorwhen possible. Specific errors document failure modes. - Use
catchwith a block for error recovery or logging; usecatch unreachableonly 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.getenvscattered 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
orelseto 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
Guides React developers in writing clean, efficient components by providing best practices for hooks, effects, and component design.
Guides Python developers in implementing type-first development with robust type checking, dataclasses, discriminated unions, and domain-specific type primitives.
Generates images using OpenAI's image generation API within the Claude Code environment.
Optimizes Tamagui configurations and styling patterns for high-performance, cross-platform React Native and web applications with v4 compiler settings.
Provides Playwright test best practices for creating resilient, maintainable tests using user-facing locators, Page Object Models, and robust testing patterns.
Enforces type-first Go development with custom types, interfaces, and patterns to create robust, self-documenting code with strong compile-time guarantees.
Fetches web content and converts HTML to clean markdown using intelligent content extraction and CSS selectors.
Enforces TypeScript best practices by guiding developers to create robust, type-safe code through compile-time validation, discriminated unions, and explicit type definitions.
Generates environment configuration files or environment variables for development projects, ensuring consistent and reproducible setup across different environments.
Retrieves Zig language documentation from Codeberg sources, standard library files, and online references via CLI commands.