Smithery Logo
MCPsSkillsDocsPricing
Login
Smithery Logo

Accelerating the Agent Economy

Resources

DocumentationPrivacy PolicySystem Status

Company

PricingAboutBlog

Connect

© 2026 Smithery. All rights reserved.

    pythoninthegrasses

    zig-best-practices

    pythoninthegrasses/zig-best-practices
    Coding
    3

    About

    SKILL.md

    Install

    Install via Skills CLI

    or add to your agent
    • Claude Code
      Claude Code
    • Codex
      Codex
    • OpenClaw
      OpenClaw
    • Cursor
      Cursor
    • Amp
      Amp
    • GitHub Copilot
      GitHub Copilot
    • Gemini CLI
      Gemini CLI
    • Kilo Code
      Kilo Code
    • Junie
      Junie
    • Replit
      Replit
    • Windsurf
      Windsurf
    • Cline
      Cline
    • Continue
      Continue
    • OpenCode
      OpenCode
    • OpenHands
      OpenHands
    • Roo Code
      Roo Code
    • Augment
      Augment
    • Goose
      Goose
    • Trae
      Trae
    • Zencoder
      Zencoder
    • Antigravity
      Antigravity
    ├─
    ├─
    └─

    About

    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.

    SKILL.md

    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:

    // 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:

    // 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:

    // 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:

    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:

    // 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:

    fn buildWidget(widget_type: []const u8) !Widget {
        return error.NotImplemented;
    }
    

    Propagate errors with try:

    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:

    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:

    fn processStatus(status: Status) ![]const u8 {
        return switch (status) {
            .active => "processing",
            .inactive => "skipped",
            _ => error.UnhandledStatus,
        };
    }
    

    Testing with memory leak detection:

    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:

    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:

    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:

    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:

    // 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:

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

    Compile-time validation:

    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):

    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):

    // Unclear what types are valid; error messages will be confusing
    fn sum(items: anytype) @TypeOf(items[0]) {
        // ...
    }
    

    Acceptable anytype for callbacks:

    /// 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:

    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:

    const ConfigError = error{
        FileNotFound,
        ParseError,
        InvalidFormat,
    };
    
    fn loadConfig(path: []const u8) ConfigError!Config {
        // implementation
    }
    

    Error handling with catch block:

    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:

    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:

    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:

    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
    • Interfacing with C libraries (raylib, SDL, curl, system APIs): See C-INTEROP.md
    • Debugging memory leaks (GPA, stack traces): See 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/
    Recommended Servers
    Vercel Grep
    Vercel Grep
    vastlint - IAB XML VAST validator and linter
    vastlint - IAB XML VAST validator and linter
    Cloudflare Workers Observability
    Cloudflare Workers Observability
    Repository
    pythoninthegrasses/mt
    Files