Zig Best Practices (0.17+)
Use this skill to make Zig code easier to reason about under failure, concurrency, and future change. Its highest-value rules cover places where otherwise plausible Zig code commonly fails: ownership, invalidated pointers, inferred contracts, unbounded input, safety-disabled builds, and version-sensitive standard-library APIs.
Scope and precedence
This is portable Zig guidance, not a project style guide.
- Read the repository's instruction files,
build.zig, andbuild.zig.zonbefore editing. Repository-local architecture, naming, and verification rules override this skill's recommendations. - Confirm the pinned Zig version. Zig's standard library changes quickly; when an API is uncertain, inspect that compiler's installed standard-library source or the matching version of the official documentation instead of recalling another release.
- Preserve a repository's established patterns unless they create a correctness, safety, or ownership defect. Consistency is valuable, but it does not excuse bugs.
Route to the relevant reference
Read only the references needed for the task:
- Read types-memory-errors.md when changing data models, allocators, lifetimes, error sets, arithmetic, casts, optionals, generics, or state machines.
- Read api-maintainability.md when adding or reviewing public APIs, modules, naming, documentation, or larger refactors.
- Read io-concurrency-security.md when code
touches
std.Io, files, networks, subprocesses, paths, threads, locks, atomics, cancellation, or externally supplied data. - Read testing-build-size.md when adding tests, fuzzing, choosing an optimization mode, preparing production builds, changing dependencies, or investigating performance or binary size.
For broad code reviews, read all four.
Working method
- Map the contract before editing. Identify ownership, borrowing, lifetimes, allocation, error behavior, external bounds, thread ownership, and build-mode assumptions. If one of these cannot be explained, investigate before changing code.
- Make invalid states harder to express. Prefer concrete domain types, enums, tagged unions, optionals, and explicit error sets over loosely related primitives.
- Keep effects visible. Allocation, blocking I/O, mutation, failure, and ownership transfer should be apparent from the API or its documentation.
- Make the smallest coherent change. Do not broaden shared semantics for one exceptional caller or mix unrelated cleanup into the change.
- Verify mechanically, then exercise behavior. Formatting and tests catch many defects; applications still need their freshly built artifact exercised through the changed path.
Blocking standards
Treat these as correctness issues rather than optional style:
- Every allocation has one clear owner and a cleanup path for success and failure.
- No pointer or slice outlives its storage or survives a mutation that may reallocate it.
- External lengths, offsets, counts, recursion, retries, queues, and captured output are validated and bounded before arithmetic, allocation, or slicing.
- Assertions and
catch unreachableprotect program-owned invariants only. Malformed external data receives normal error handling in every build mode. - Integer conversions and arithmetic have explicit range and overflow reasoning.
- Shared mutable state has an owner and synchronization policy; cancellation and shutdown cannot leave threads or resources behind.
- Public contracts state ownership and do not accidentally expose
anyerror,anytype, internal mutable storage, or version-specific implementation details. - The code compiles and tests with the repository's pinned Zig version.
Common Zig 0.17+ gotchas
- Allocator-explicit containers:
std.ArrayList(T)is allocator-explicit in 0.17. Initialize with.empty, then pass the allocator to operations anddeinit. Storing an allocator is still valid when an object owns allocations across method calls; document that lifetime rather than banning the pattern. - Interior-pointer invalidation: growing a list or hash table may relocate storage. Reacquire pointers and slices after mutation, or reserve capacity before taking them.
undefinedis not initialization: reading it is illegal behavior. Debug poison values such as0xaaare implementation details, not values code may depend on.- Error-set extremes: inferred error sets are useful inside implementations; named
error sets make stable public boundaries reviewable.
anyerroris appropriate only when intentionally erasing the set is part of the contract. - Casts do not prove validity: use
std.math.castfor fallible narrowing, or establish and document the invariant before@intCast. Child.Termtags are lowercase: handle.exited,.signal,.stopped, and.unknownexhaustively. Do not copy pre-0.16 examples with capitalized tags.- I/O-aware locking: Zig 0.17 exposes
std.Io.Mutex;lockaccepts anstd.Ioand is a cancellation point. Choose cancelable versus uncancelable locking deliberately. - The standard library is version-sensitive: for example, Zig 0.17 exports
std.compress. Verify the pinned standard library before claiming an API was removed. For what Zig 0.17 removed, deprecated, or silently changed, use thezig-0.17skill. - Test discovery follows imports: colocated tests run only when their source file is reachable from the selected test root.
- Fuzzing requires a target: define a
std.testing.fuzzentry point; a--fuzzflag does not turn ordinary unit tests into useful fuzz targets.
Review judgment
Prioritize findings in this order:
- Memory safety, illegal behavior, races, deadlocks, resource leaks, and corrupted state.
- Incorrect ownership, error, external-input, cancellation, or public API contracts.
- Maintainability problems that make future defects likely.
- Performance or binary-size opportunities supported by measurement.
- Pure style preferences not already decided by the repository.
Report correctness and safety findings as blocking. Report defensible design alternatives as considerations with their tradeoff; do not present taste as a defect.
Verification gate
Use the repository's documented commands first. If it has none, establish a baseline by format-checking the project-owned Zig paths, then building and testing:
zig fmt --check <project-owned-zig-paths>
zig build
zig build test
Add only checks relevant to the claim:
- Run a defined fuzz target when parser or untrusted-byte handling changed.
- Test Debug and ReleaseSafe when behavior could depend on safety checks or optimization.
- Cross-compile when portability is claimed.
- Build with ReleaseSmall or ReleaseFast when evaluating their size or speed.
- Run the freshly built executable or library consumer when runtime integration changed.
- Compare performance or binary size only with the same target, options, stripping, features, and measurement method.
If a required command cannot run, state exactly what remains unverified. Never turn a read-only review into a claim that runtime behavior was proven.
Final self-check
- Ownership and borrowed lifetimes are visible.
- Every fallible acquisition has failure-path cleanup.
- Domain states, errors, arithmetic, and external bounds are explicit.
- Public APIs expose the minimum surface and document effects.
- I/O, concurrency, cancellation, and shutdown behavior are accounted for when relevant.
- Tests cover success, boundaries, and meaningful failure paths.
- Build-mode, performance, and size claims are measured rather than assumed.
- Repository instructions and the pinned Zig version were followed.