zig-best-practices
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.
What this skill does
# 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");
Related in Writing & Docs
jax-development
IncludedUse this skill when the user is writing, debugging, profiling, refactoring, reviewing, benchmarking, parallelising, exporting, or explaining JAX code, or when they mention JAX, jax.numpy, jit, grad, value_and_grad, vmap, scan, lax, random keys, pytrees, jax.Array, sharding, Mesh, PartitionSpec, NamedSharding, pmap, shard_map, Pallas, XLA, StableHLO, checkify, profiler, or the JAX repo. It helps turn NumPy or PyTorch-style code into pure functional JAX, fix tracer/control-flow/shape/PRNG bugs, remove recompiles and host-device syncs, choose transforms and sharding strategies, inspect jaxpr/lowering/IR, and benchmark compiled code correctly.
nature-article-writer
IncludedDrafts, rewrites, diagnostically critiques, and style-calibrates primary research manuscripts for Nature and Nature Portfolio journals. Use when the user wants a Nature-style title, summary paragraph or abstract, introduction, results, discussion, methods, figure legends, presubmission enquiry, cover letter, reviewer response, or when a scientific draft sounds generic, jargon-heavy, structurally weak, or AI-ish and needs precise, broad-reader-friendly prose without inventing data, analyses, or references. Best for primary research articles and letters rather than reviews or press releases unless explicitly adapting one.
deckrd
IncludedDocument-driven framework that derives requirements, specifications, implementation plans, and executable tasks from goals through structured AI dialogue. Use when user says "write requirements", "create spec", "plan implementation", "derive tasks", "structure this feature", "break down into tasks", or "document this module". Also use for reverse engineering existing code into docs (/deckrd rev). Do NOT use for direct code writing — use /deckrd-coder after tasks are generated. Do NOT use when the user only wants to run or fix existing code without planning.
clinical-decision-support
IncludedGenerate professional clinical decision support (CDS) documents for pharmaceutical and clinical research settings, including patient cohort analyses (biomarker-stratified with outcomes) and treatment recommendation reports (evidence-based guidelines with decision algorithms). Supports GRADE evidence grading, statistical analysis (hazard ratios, survival curves, waterfall plots), biomarker integration, and regulatory compliance. Outputs publication-ready LaTeX/PDF format optimized for drug development, clinical research, and evidence synthesis.
handling-sf-data
IncludedSalesforce data operations with 130-point scoring. Use this skill to create, update, delete, bulk import/export, generate test data, and clean up org records using sf CLI and anonymous Apex. TRIGGER when: user creates test data, performs bulk import/export, uses sf data CLI commands, needs data factory patterns for Apex tests, or needs to seed/clean records in a Salesforce org. DO NOT TRIGGER when: SOQL query writing only (use querying-soql), Apex test execution (use running-apex-tests), or metadata deployment (use deploying-metadata).
accelint-ac-to-playwright
IncludedConvert and validate acceptance criteria for Playwright test automation. Use when user asks to (1) review/evaluate/check if AC are ready for automation, (2) assess if AC can be converted as-is, (3) validate AC quality for Playwright, (4) turn AC into tests, (5) generate tests from acceptance criteria, (6) convert .md bullets or .feature Gherkin files to Playwright specs, (7) create test automation from requirements. Handles both bullet-style markdown and Gherkin syntax with JSON test plan generation and validation.