Zig Language Reference (v0.15.2)
Zig evolves rapidly. Training data contains outdated patterns that cause compilation errors. This skill documents breaking changes and correct modern patterns.
Design Principles
Type-First Development
Define types and function signatures before implementation. Let the compiler guide completeness:
- Define data structures (structs, unions, error sets)
- Define function signatures (parameters, return types, error unions)
- Implement to satisfy types
- Validate at compile-time
Make Illegal States Unrepresentable
Use Zig's type system to prevent invalid states at compile time:
- Tagged unions over structs with optional fields — prevent impossible state combinations
- Explicit error sets over
anyerror— document exactly which failures can occur - Distinct types via
enum(u64) { _ }— prevent mixing up IDs (user_id vs order_id) - Comptime validation with
@compileError()— catch invalid configurations at build time
Module Structure
Larger cohesive files are idiomatic in Zig. Keep related code together — tests alongside implementation, comptime generics at file scope, visibility controlled by pub. Split files only for genuinely separate concerns. The std library demonstrates this with files like std/mem.zig containing thousands of cohesive lines.
Memory Ownership
- Pass allocators explicitly — never use global state for allocation
- Use
deferimmediately after acquiring a resource — cleanup next to acquisition - Name allocators by contract:
gpa(caller must free),arena(bulk-free at boundary),scratch(never escapes) - Prefer
constovervar— immutability signals intent and enables optimizations - Prefer slices over raw pointers — bounds safety
Critical: Removed Features (0.15.x)
usingnamespace - REMOVED
// WRONG - compile error
pub usingnamespace @import("other.zig");
// CORRECT - explicit re-export
const other = @import("other.zig");
pub const foo = other.foo;
async/await - REMOVED
Keywords removed from language. Async I/O support is planned for future releases.
std.BoundedArray - REMOVED
Use std.ArrayList with initBuffer:
var buffer: [8]i32 = undefined;
var stack = std.ArrayList(i32).initBuffer(&buffer);
std.RingBuffer, std.fifo.LinearFifo - REMOVED
Use std.Io.Reader/std.Io.Writer ring buffers instead.
std.io.SeekableStream, std.io.BitReader, std.io.BitWriter - REMOVED
std.fmt.Formatter - REMOVED
Replaced by std.fmt.Alt.
Undefined Behavior Restrictions (0.15.x)
Arithmetic on undefined is now illegal. Only operators that can never trigger Illegal Behavior permit undefined as operand.
// WRONG - compile error in 0.15.x
var n: usize = undefined;
while (condition) : (n += 1) {} // ERROR: use of undefined value
// CORRECT - explicit initialization required
var n: usize = 0;
while (condition) : (n += 1) {}
// OK - space reservation (no arithmetic)
var buffer: [256]u8 = undefined;
Critical: I/O API Rewrite ("Writergate")
The entire std.io API changed. New std.Io.Writer and std.Io.Reader are non-generic with buffer in the interface.
Writing
// WRONG - old API
const stdout = std.io.getStdOut().writer();
try stdout.print("Hello\n", .{});
// CORRECT - new API: provide buffer, access .interface, flush
var buf: [4096]u8 = undefined;
var stdout_writer = std.fs.File.stdout().writer(&buf);
const stdout = &stdout_writer.interface;
try stdout.print("Hello\n", .{});
try stdout.flush(); // REQUIRED!
Reading
// Reading from file
var buf: [4096]u8 = undefined;
var file_reader = file.reader(&buf);
const r = &file_reader.interface;
// Read line by line (takeDelimiter returns null at EOF)
while (try r.takeDelimiter('\n')) |line| {
// process line (doesn't include '\n')
}
// Read binary data
const header = try r.takeStruct(Header, .little);
const value = try r.takeInt(u32, .big);
Fixed Buffer Writer (no file)
var buf: [256]u8 = undefined;
var w: std.Io.Writer = .fixed(&buf);
try w.print("Hello {s}", .{"world"});
const result = w.buffered(); // "Hello world"
Fixed Reader (from slice)
var r: std.Io.Reader = .fixed("hello\nworld");
const line = (try r.takeDelimiter('\n')).?; // "hello" (returns null at EOF)
Removed: BufferedWriter, CountingWriter, std.io.bufferedWriter()
Deprecated: GenericWriter, GenericReader, AnyWriter, AnyReader, FixedBufferStream
New: std.Io.Writer, std.Io.Reader - non-generic, buffer in interface
Replacements:
CountingWriter->std.Io.Writer.Discarding(has.fullCount())BufferedWriter-> buffer provided to.writer(&buf)call- Allocating output ->
std.Io.Writer.Allocating
Critical: Build System (0.15.x)
root_source_file is REMOVED from addExecutable/addLibrary/addTest. Use root_module:
// WRONG - removed field
b.addExecutable(.{
.name = "app",
.root_source_file = b.path("src/main.zig"), // ERROR
.target = target,
});
// CORRECT
b.addExecutable(.{
.name = "app",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
}),
});
Module imports changed:
// WRONG (old API)
exe.addModule("helper", helper_mod);
// CORRECT
exe.root_module.addImport("helper", helper_mod);
Libraries: addSharedLibrary → addLibrary with .linkage:
// WRONG - removed function
const lib = b.addSharedLibrary(.{ .name = "mylib", ... });
// CORRECT - unified addLibrary with linkage field
const lib = b.addLibrary(.{
.name = "mylib",
.linkage = .dynamic, // or .static (default)
.root_module = b.createModule(.{
.root_source_file = b.path("src/lib.zig"),
.target = target,
.optimize = optimize,
}),
});
Adding dependency modules:
const dep = b.dependency("lib", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("lib", dep.module("lib"));
See std.Build reference for complete build system documentation.
Critical: Container Initialization
Never use .{} for containers. Use .empty or .init:
// WRONG - deprecated
var list: std.ArrayList(u32) = .{};
var gpa: std.heap.DebugAllocator(.{}) = .{};
// CORRECT - use .empty for empty collections
var list: std.ArrayList(u32) = .empty;
var map: std.AutoHashMapUnmanaged(u32, u32) = .empty;
// CORRECT - use .init for stateful types with internal config
var gpa: std.heap.DebugAllocator(.{}) = .init;
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
ArrayList: Unmanaged by Default
The old std.ArrayList (with allocator stored in struct) is now std.array_list.Managed.
The new std.ArrayList is unmanaged — no allocator field, pass allocator to every method:
// NEW default: unmanaged (no allocator field)
var list: std.ArrayList(u32) = .empty;
try list.append(allocator, 42);
list.deinit(allocator);
// If you want old behavior (allocator in struct), use Managed:
var list = std.array_list.Managed(u32).init(allocator);
try list.append(42); // no allocator arg needed
list.deinit();
HashMap: Managed vs Unmanaged
Same pattern applies — unmanaged variants require allocator per operation:
// Unmanaged (no internal allocator)
var map: std.StringHashMapUnmanaged(u32) = .empty;
try map.put(allocator, "key", 42);
map.deinit(allocator);
// Managed (stores allocator internally)
var map = std.StringHashMap(u32).init(allocator);
try map.put("key", 42);
map.deinit();
Naming Changes
std.ArrayListUnmanaged->std.ArrayList(Unmanaged is now default, old name deprecated)std.heap.GeneralPurposeAllocator->std.heap.DebugAllocator(GPA alias still works)
Linked Lists: Generic Parameter Removed
// WRONG - old API
const Node = std.DoublyLinkedList(MyData).Node;
// CORRECT - non-generic, use @fieldParentPtr
const MyNode = struct {
node: std.DoublyLinkedList.Node,
data: MyData,
};
// Access: const my_node = @fieldParentPtr("node", node_ptr);
Process API: Term is Tagged Union
// WRONG - direct field access removed
if (result.term.Exited != 0) {}
// CORRECT - pattern match
switch (result.term) {
.exited => |code| if (code != 0) { /* handle */ },
else => {},
}
Critical: Format Strings (0.15.x)
{f} required to call format methods:
// WRONG - ambiguous error
std.debug.print("{}", .{std.zig.fmtId("x")});
// CORRECT
std.debug.print("{f}", .{std.zig.fmtId("x")});
Format method signature changed:
// OLD - wrong
pub fn format(self: @This(), comptime fmt: []const u8, opts: std.fmt.FormatOptions, writer: anytype) !void
// NEW - correct
pub fn format(self: @This(), writer: *std.Io.Writer) std.Io.Writer.Error!void
Breaking Changes (0.14.0+)
@branchHint replaces @setCold
// WRONG
@setCold(true);
// CORRECT
@branchHint(.cold); // Must be first statement in block
@export takes pointer
// WRONG
@export(foo, .{ .name = "bar" });
// CORRECT
@export(&foo, .{ .name = "bar" });
Inline asm clobbers are typed
// WRONG
: "rcx", "r11"
// CORRECT
: .{ .rcx = true, .r11 = true }
@fence - REMOVED
Use stronger atomic orderings or RMW operations instead.
@typeInfo fields now lowercase
// WRONG - old PascalCase (compile error)
if (@typeInfo(T) == .Struct) { ... }
if (@typeInfo(T) == .Slice) { ... }
if (@typeInfo(T) == .Int) { ... }
// CORRECT - lowercase, keywords escaped with @""
if (@typeInfo(T) == .@"struct") { ... }
if (@typeInfo(T) == .@"enum") { ... }
if (@typeInfo(T) == .@"union") { ... }
if (@typeInfo(T) == .@"opaque") { ... }
if (@typeInfo(T) == .slice) { ... }
if (@typeInfo(T) == .int) { ... }
if (@typeInfo(T) == .bool) { ... }
if (@typeInfo(T) == .pointer) { ... }
// Accessing fields:
const fields = @typeInfo(T).@"struct".fields;
const tag_type = @typeInfo(T).@"enum".tag_type;
Decl Literals (0.14.0+)
.identifier syntax works for declarations:
const S = struct {
x: u32,
const default: S = .{ .x = 0 };
fn init(v: u32) S { return .{ .x = v }; }
};
const a: S = .default; // S.default
const b: S = .init(42); // S.init(42)
const c: S = try .init(1); // works with try
Labeled Switch (0.14.0+)
State machines use continue :label:
state: switch (initial) {
.idle => continue :state .running,
.running => if (done) break :state result else continue :state .running,
.error => return error.Failed,
}
Non-exhaustive Enum Switch (0.15.x)
Can mix explicit tags with _ and else:
switch (value) {
.a, .b => {},
else => {}, // other named tags
_ => {}, // unnamed integer values
}
Critical: HTTP API Reworked (0.15.x)
HTTP client/server completely restructured — depends only on I/O streams, not networking:
// Server now takes Reader/Writer interfaces, not connection directly
var recv_buffer: [4000]u8 = undefined;
var send_buffer: [4000]u8 = undefined;
var conn_reader = connection.stream.reader(&recv_buffer);
var conn_writer = connection.stream.writer(&send_buffer);
var server = std.http.Server.init(
conn_reader.interface(),
&conn_writer.interface,
);
Note: HTTP client API is still rapidly evolving. For stability-critical code, consider shelling out to curl.
Quick Fixes
| Error | Fix |
|---|---|
no field 'root_source_file' | Use root_module = b.createModule(.{...}) |
use of undefined value | Arithmetic on undefined is now illegal — initialize explicitly |
type 'f32' cannot represent integer | Use float literal: 123_456_789.0 not 123_456_789 |
ambiguous format string | Use {f} for format methods |
no field 'append' on ArrayList | Pass allocator: list.append(allocator, val) (unmanaged default) |
expected 2 arguments, found 1 on ArrayList | Add allocator param: .append(allocator, val), .deinit(allocator) |
BoundedArray not found | Use std.ArrayList(T).initBuffer(&buf) |
GenericWriter/GenericReader | Use std.Io.Writer/std.Io.Reader |
missing .flush() — no output | Always call try writer.flush() after writing |
enum has no member named 'Struct' | @typeInfo fields now lowercase: .@"struct", .slice, .int |
no field named 'encode' on base64 | Use std.base64.standard.Encoder.encode() |
no field named 'open' on HTTP | Use client.request() or client.fetch() |
expected error union, found Signature | Ed25519.Signature.fromBytes() doesn't return error — remove try |
addSharedLibrary not found | Use b.addLibrary(.{ .linkage = .dynamic, ... }) |
Verification Workflow
After writing or modifying Zig code, verify with this sequence:
zig build— catch compilation errors, match against Quick Fixes abovezig build test— run unit testszig build -Doptimize=ReleaseFast test— detect undefined behavior (UB checks enabled in optimized builds)
Development speed tips:
zig build --watch -fincremental— incremental compilation, rebuilds on file change- 0.15.x uses self-hosted x86_64 backend by default — ~5x faster Debug builds than LLVM
Common Pitfalls
- Forgetting
defer/errdefercleanup — place cleanup immediately after resource acquisition - Using
anyerrorinstead of specific error sets — explicit sets document failure modes - Ignoring error unions — handle or propagate, never discard
- Missing
errdeferafter allocations in multi-step init — partial construction leaks - Expecting comptime side effects — comptime code is evaluated lazily
- Unhandled integer overflow — Zig traps on overflow in debug builds
- Missing null terminators for C strings — use
:0sentinel slices:[:0]const u8 - Using
anytypewhencomptime T: typeworks — explicit types produce clearer errors - Scoped loggers: always define per-module
const log = std.log.scoped(.my_module);for filterable logging
Learning Resources
Production Zig codebases worth studying:
- Bun — JS runtime (~200k+ lines), async I/O, FFI, system calls
- Ghostty — Terminal emulator, cross-platform, GPU rendering
- TigerBeetle — Financial DB, deterministic execution, VOPR fuzzing
- Mach Engine — Game engine, graphics, ECS
- Sig — Solana validator, high-performance networking
Language References
Load these references when working with core language features:
Code Style
- Style Guide - Official Zig naming conventions (TitleCase types, camelCase functions, snake_case variables), whitespace rules, doc comment guidance, redundancy avoidance,
zig fmt
Language Basics & Built-ins
- Language Basics - Core language: types, control flow (if/while/for/switch), error handling (try/catch/errdefer), optionals, structs, enums, unions, pointers, slices, comptime, functions
- Built-in Functions - All
@built-ins: type casts (@intCast, @bitCast, @ptrCast), arithmetic (@addWithOverflow, @divExact), bit ops (@clz, @popCount), memory (@memcpy, @sizeOf), atomics (@atomicRmw, @cmpxchgWeak), introspection (@typeInfo, @TypeOf, @hasDecl), SIMD (@Vector, @splat, @reduce), C interop (@cImport, @export)
Standard Library References
Load these references when working with specific modules:
Memory & Slices
- std.mem - Slice search/compare, split/tokenize, alignment, endianness, byte conversion
Text & Encoding
- std.fmt - Format strings, integer/float parsing, hex encoding, custom formatters,
{f}specifier (0.15.x) - std.ascii - ASCII character classification (isAlpha, isDigit), case conversion, case-insensitive comparison
- std.unicode - UTF-8/UTF-16 encoding/decoding, codepoint iteration, validation, WTF-8 for Windows
- std.base64 - Base64 encoding/decoding (standard, URL-safe, with/without padding)
Math & Random
- std.math - Floating-point ops, trig, overflow-checked arithmetic, constants, complex numbers, big integers
- std.Random - PRNGs (Xoshiro256, Pcg), CSPRNGs (ChaCha), random integers/floats/booleans, shuffle, distributions
- std.hash - Non-cryptographic hash functions (Wyhash, XxHash, FNV, Murmur, CityHash), checksums (CRC32, Adler32), auto-hashing
SIMD & Vectorization
- std.simd - SIMD vector utilities: optimal vector length, iota/repeat/join/interlace patterns, element shifting/rotation, parallel searching, prefix scans, branchless selection
Time & Timing
- std.time - Wall-clock timestamps, monotonic Instant/Timer, epoch conversions, calendar utilities (year/month/day), time unit constants
- std.Tz - TZif timezone database parsing (RFC 8536), UTC offsets, DST rules, timezone abbreviations, leap seconds
Sorting & Searching
- std.sort - Sorting algorithms (pdq, block, heap, insertion), binary search, min/max
Core Data Structures
- std.ArrayList - Dynamic arrays, vectors, BoundedArray replacement
- std.HashMap / AutoHashMap - Hash maps, string maps, ordered maps
- std.ArrayHashMap - Insertion-order preserving hash map, array-style key/value access
- std.MultiArrayList - Struct-of-arrays for cache-efficient struct storage
- std.SegmentedList - Stable pointers, arena-friendly, non-copyable types
- std.DoublyLinkedList / SinglyLinkedList - Intrusive linked lists, O(1) insert/remove
- std.PriorityQueue - Binary heap, min/max extraction, task scheduling
- std.PriorityDequeue - Min-max heap, double-ended priority extraction
- std.Treap - Self-balancing BST, ordered keys, min/max/predecessor
- std.bit_set - Bit sets (Static, Dynamic, Integer, Array), set operations, iteration
- std.BufMap / BufSet - String-owning maps and sets, automatic key/value memory management
- std.StaticStringMap - Compile-time optimized string lookup, perfect hash for keywords
- std.enums - EnumSet, EnumMap, EnumArray: bit-backed enum collections
Allocators
- std.heap - Allocator selection guide, ArenaAllocator, DebugAllocator, FixedBufferAllocator, MemoryPool, SmpAllocator, ThreadSafeAllocator, StackFallbackAllocator, custom allocator implementation
I/O & Files
- std.io - Reader/Writer API (0.15.x): buffered I/O, streaming, binary data, format strings
- std.fs - File system: files, directories, iteration, atomic writes, paths
- std.tar - Tar archive reading/writing, extraction, POSIX ustar, GNU/pax extensions
- std.zip - ZIP archive reading/extraction, ZIP64 support, store/deflate compression
- std.compress - Compression: DEFLATE (gzip, zlib), Zstandard, LZMA, LZMA2, XZ decompression/compression
Networking
- std.http - HTTP client/server, TLS, connection pooling, compression, WebSocket
- std.net - TCP/UDP sockets, address parsing, DNS resolution
- std.Uri - URI parsing/formatting (RFC 3986), percent-encoding/decoding, relative URI resolution
Process Management
- std.process - Child process spawning, environment variables, argument parsing, exec
OS-Specific APIs
- std.os - OS-specific APIs: Linux syscalls, io_uring, Windows NT APIs, WASI, direct platform access
- std.c - C ABI types and libc bindings: platform-specific types (fd_t, pid_t, timespec), errno values, socket/signal/memory types, fcntl/open flags, FFI with C libraries
Concurrency
- std.Thread - Thread spawning, Mutex, RwLock, Condition, Semaphore, WaitGroup, thread pools
- std.atomic - Lock-free atomic operations: Value wrapper, fetch-and-modify (add/sub/and/or/xor), compare-and-swap, atomic ordering semantics, spin loop hints, cache line sizing
Patterns & Best Practices
- Zig Patterns - Load when writing new code or reviewing code quality. Comprehensive best practices extracted from the Zig standard library: quick patterns (memory/allocators, file I/O, HTTP, JSON, testing, build system) plus idiomatic code patterns covering syntax (closures, context pattern, options structs, destructuring), polymorphism (duck typing, generics, custom formatting, dynamic/static dispatch), safety (diagnostics, error payloads, defer/errdefer, compile-time assertions), and performance (const pointer passing)
- Production Patterns - Load when building large-scale Zig systems or optimizing performance. Real-world patterns from Bun, Ghostty, TigerBeetle: modular build systems, CPU feature locking, pre-allocated message pools, counting allocators, SIMD with scalar fallback, intrusive linked lists, cache-line aligned SoA, work-stealing thread pools, SmolStr (15-byte SSO), comptime string maps, EnumUnionType generation, VOPR fuzzing, snapshot testing, edge-biased fuzz generation, platform abstraction facades, Objective-C bridges, opaque C wrappers with RAII, packed struct bitfields, Result union types, radix sort, tournament trees
- MCP Server Patterns - Load when building MCP servers, LSP bridges, JSON-RPC services, or protocol translators in Zig. Patterns from zig-mcp: newline-delimited vs Content-Length transport, thread-based request correlation with ResetEvent, arena-per-request memory, child process lifecycle with pipe ownership transfer, tool registry with function pointers, std.json.Stringify for manual JSON building, lazy document sync with double-check locking, graceful degradation, auto-reconnect on crash, comptime schema generation, file URI encoding, common serialization gotchas
- Code Review - Load when reviewing Zig code. Systematic checklist organized by confidence level: ALWAYS FLAG (removed features, changed syntax, API changes), FLAG WITH CONTEXT (exception safety bugs, missing flush, allocator issues), SUGGEST (style improvements). Includes migration examples for 0.14/0.15 breaking changes
Serialization
- std.json - JSON parsing, serialization, dynamic values, streaming, custom parse/stringify
- std.zon - ZON (Zig Object Notation) parsing and serialization for build.zig.zon, config files, data interchange
Testing & Debug
- std.testing - Unit test assertions and utilities
- std.debug - Panic, assert, stack traces, hex dump, format specifiers
- std.log - Scoped logging with configurable levels and output
Metaprogramming
- Comptime Reference - Comptime fundamentals, type reflection (
@typeInfo/@Type/@TypeOf), loop variants (comptime forvsinline for), branch elimination, type generation, comptime limitations - std.meta - Type introspection, field iteration, stringToEnum, generic programming
Compiler Utilities
- std.zig - AST parsing, tokenization, source analysis, linters, formatters, ZON parsing
Security & Cryptography
- std.crypto - Hashing (SHA2, SHA3, Blake3), AEAD (AES-GCM, ChaCha20-Poly1305), signatures (Ed25519, ECDSA), key exchange (X25519), password hashing (Argon2, scrypt, bcrypt), secure random, timing-safe operations
Build System
- std.Build - Build system: build.zig, modules, dependencies, build.zig.zon, steps, options, testing, C/C++ integration
Interoperability
- C Interop - Exporting C-compatible APIs:
export fn, C calling convention, building static/dynamic libraries, creating headers, macOS universal binaries, XCFramework for Swift/Xcode, module maps
Tooling
ZLS (Zig Language Server)
IDE support via Language Server Protocol. Provides autocomplete, go-to-definition, hover docs, diagnostics.
Version matching rule: Use ZLS release matching your Zig release (0.15.x ZLS for 0.15.x Zig). Nightly Zig needs nightly ZLS.
Installation:
# VS Code: install "Zig Language" extension (includes ZLS)
# Manual / other editors:
# Download from https://github.com/zigtools/zls/releases
# Or build from source:
git clone https://github.com/zigtools/zls
cd zls && git checkout 0.15.0 # match your Zig version
zig build -Doptimize=ReleaseSafe
# Configure:
zls --config
Editor support: VS Code, Neovim (nvim-lspconfig), Helix, JetBrains, Emacs (lsp-mode), Sublime Text, Kate.
Key features:
- Autocomplete with semantic analysis
- Go-to-definition, find references
- Hover documentation
- Diagnostics (compile errors inline)
- Filesystem completions inside
@import("")strings stdandbuiltinmodule path completions- Snippets for common declarations
anyzig (Version Manager)
Universal Zig version manager — run any Zig version from any project. Replaces manual version switching.
Source: https://github.com/marler8997/anyzig
How it works:
- Reads
minimum_zig_versionfrombuild.zig.zon(searches up directory tree) - Auto-downloads needed compiler version into global cache
- Invokes the correct
zigtransparently
Installation:
# macOS (Apple Silicon)
curl -L https://marler8997.github.io/anyzig/aarch64-macos/anyzig.tar.xz | tar xJ
mv zig /usr/local/bin/zig # replaces/shadows system zig
# macOS (Intel)
curl -L https://marler8997.github.io/anyzig/x86_64-macos/anyzig.tar.xz | tar xJ
mv zig /usr/local/bin/zig
# Linux (x86_64)
curl -L https://marler8997.github.io/anyzig/x86_64-linux/anyzig.tar.xz | tar xJ
sudo mv zig /usr/local/bin/zig
Usage:
# Automatic — reads build.zig.zon minimum_zig_version:
cd myproject && zig build
# Manual version override:
zig 0.13.0 build-exe myproject.zig
zig 0.15.2 build
# Mach engine versions supported:
# Reads .mach_zig_version from build.zig.zon
# Format: 2024.10.0-mach
# anyzig-specific commands:
zig any --help
build.zig.zon version field:
.{
.name = .myproject,
.version = "0.1.0",
.minimum_zig_version = "0.15.2",
// ...
}