← Back to all guides

Zig TLDR

A rapid reference guide to the Zig programming language. Everything you need to know, distilled.

v0.15.1 — November 2025

What is Zig?

Zig is a systems programming language designed to be a "better C" — simpler, safer, and more maintainable while remaining just as fast. It compiles to native code and can interoperate with C libraries seamlessly.

No Hidden Control Flow

No operator overloading, no hidden allocations, no implicit function calls. What you see is what you get.

Compile-Time Execution

Run any function at compile time with comptime. No macros needed — just regular Zig code.

Manual Memory

No garbage collector. You control allocations explicitly with allocator interfaces.

Cross-Compilation

First-class cross-compilation to 50+ targets. Build for any platform from any platform.

Basics

Hello World

const std = @import("std");

pub fn main() void {
    std.debug.print("Hello, {s}!\n", .{"world"});
}

Variables

// Constants (immutable)
const x: i32 = 42;
const y = 42;         // type inferred

// Variables (mutable)
var z: i32 = 0;
z += 1;

// Undefined (uninitialized)
var buf: [256]u8 = undefined;

Functions

fn add(a: i32, b: i32) i32 {
    return a + b;
}

// Public function (exported)
pub fn multiply(a: i32, b: i32) i32 {
    return a * b;
}

// Function that can fail
fn divide(a: i32, b: i32) !i32 {
    if (b == 0) return error.DivisionByZero;
    return @divTrunc(a, b);
}

Types

Primitive Types

Category Types Notes
Signed Integers i8, i16, i32, i64, i128, isize Also arbitrary: i7, i123, etc.
Unsigned Integers u8, u16, u32, u64, u128, usize Also arbitrary: u3, u48, etc.
Floats f16, f32, f64, f80, f128 IEEE 754 floats
Boolean bool true or false
Void void Zero-sized type
Comptime comptime_int, comptime_float Arbitrary precision at compile time

Arrays & Slices

// Fixed-size array
const arr: [5]i32 = .{ 1, 2, 3, 4, 5 };

// Slice (pointer + length)
const slice: []const i32 = arr[1..4];  // {2, 3, 4}

// String literals are []const u8
const str: []const u8 = "hello";

// Sentinel-terminated (null-terminated)
const c_str: [*:0]const u8 = "hello";

Optionals

// Optional type: can be null
var maybe: ?i32 = 42;
maybe = null;

// Unwrap with orelse
const value = maybe orelse 0;

// Unwrap with if
if (maybe) |val| {
    std.debug.print("Got: {}\n", .{val});
}

Pointers

var x: i32 = 42;

// Single-item pointer
const ptr: *i32 = &x;
ptr.* = 100;  // dereference

// Many-item pointer (unknown length)
const many: [*]i32 = &arr;

// Optional pointer
var opt_ptr: ?*i32 = null;
opt_ptr = &x;

Control Flow

If / Else

const result = if (x > 0) "positive" else "non-positive";

if (optional_value) |value| {
    // value is unwrapped here
} else {
    // optional was null
}

While

var i: usize = 0;
while (i < 10) : (i += 1) {
    // loop body
}

// With optional unwrapping
while (iterator.next()) |item| {
    // process item
}

For

// Iterate over slice/array
for (items) |item| {
    std.debug.print("{}\n", .{item});
}

// With index
for (items, 0..) |item, i| {
    std.debug.print("[{}] = {}\n", .{i, item});
}

// Range (0 to 9)
for (0..10) |i| {
    _ = i;
}

Switch

const result = switch (x) {
    0 => "zero",
    1...9 => "single digit",
    10, 100 => "ten or hundred",
    else => "other",
};

// Capture value
switch (tagged_union) {
    .some_tag => |value| process(value),
    else => {},
}

Labeled Blocks

const result = blk: {
    if (condition) break :blk 42;
    break :blk 0;
};

Error Handling

Zig uses explicit error unions instead of exceptions. Errors are values, not control flow.

Error Unions

// Define error set
const FileError = error {
    NotFound,
    PermissionDenied,
    EndOfFile,
};

// Function returning error union
fn readFile(path: []const u8) FileError![]u8 {
    if (path.len == 0) return error.NotFound;
    // ...
}

// Inferred error set
fn process() !void {
    // ! means inferred error set
}

Handling Errors

// try: propagate error up
const data = try readFile("config.txt");

// catch: handle error
const data = readFile("config.txt") catch |err| {
    std.debug.print("Error: {}\n", .{err});
    return;
};

// catch with default value
const data = readFile("config.txt") catch "default";

// if-else with error capture
if (readFile("config.txt")) |data| {
    // success
} else |err| {
    // handle error
}

defer & errdefer

fn process() !void {
    const file = try openFile();
    defer file.close();  // Always runs on scope exit

    const buffer = try allocator.alloc(u8, 1024);
    errdefer allocator.free(buffer);  // Only runs on error

    try riskyOperation();  // If this fails, buffer is freed
    // If we get here, buffer is NOT freed (success path)
}
defer statements execute in reverse order. Use them to ensure cleanup happens even when errors occur.

Memory Management

Zig has no garbage collector. You manage memory explicitly using allocator interfaces.

Allocators

const std = @import("std");

// General purpose allocator (recommended)
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();

// Allocate single item
const ptr = try allocator.create(MyStruct);
defer allocator.destroy(ptr);

// Allocate array/slice
const slice = try allocator.alloc(u8, 1024);
defer allocator.free(slice);

Common Allocators

GeneralPurposeAllocator
Default choice. Tracks leaks in debug mode. Thread-safe.
ArenaAllocator
Fast bump allocator. Free everything at once. Great for request handling.
FixedBufferAllocator
Allocate from a fixed buffer. No syscalls. Stack-friendly.
page_allocator
Direct OS pages. Use for large allocations or as backing for other allocators.

Compile-Time Execution

Zig's comptime lets you run any code at compile time. This replaces macros and enables powerful metaprogramming.

Basic Comptime

// Compute at compile time
const factorial = comptime blk: {
    var result: u64 = 1;
    for (1..11) |i| {
        result *= i;
    }
    break :blk result;  // 3628800
};

// Array size from comptime
const size = comptime calculateSize();
var buffer: [size]u8 = undefined;

Generic Functions

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

const result = max(i32, 5, 10);  // 10

Type Reflection

fn printFields(comptime T: type) void {
    const info = @typeInfo(T);
    inline for (info.@"struct".fields) |field| {
        std.debug.print("Field: {s}\n", .{field.name});
    }
}

Comptime String Operations

// String concatenation (comptime only)
const greeting = "Hello, " ++ "World!";

// Array repetition (comptime only)
const dashes = "-" ** 40;  // "----------------------------------------"
++ and ** operators only work at compile time. For runtime string operations, use std.mem functions.

Structs & More

Structs

const Point = struct {
    x: f32,
    y: f32,

    // Method
    pub fn distance(self: Point, other: Point) f32 {
        const dx = self.x - other.x;
        const dy = self.y - other.y;
        return @sqrt(dx * dx + dy * dy);
    }

    // Associated function (no self)
    pub fn origin() Point {
        return .{ .x = 0, .y = 0 };
    }
};

const p = Point{ .x = 3, .y = 4 };
const dist = p.distance(Point.origin());

Enums

const Color = enum {
    red,
    green,
    blue,

    pub fn isWarm(self: Color) bool {
        return self == .red;
    }
};

const c: Color = .green;

Tagged Unions

const Value = union(enum) {
    int: i64,
    float: f64,
    string: []const u8,
    none,

    pub fn format(self: Value) []const u8 {
        return switch (self) {
            .int => "integer",
            .float => "float",
            .string => "string",
            .none => "none",
        };
    }
};

const v = Value{ .int = 42 };

Standard Library Highlights

Common Imports

const std = @import("std");

// Commonly used
const mem = std.mem;           // Memory utilities
const fmt = std.fmt;           // Formatting
const fs = std.fs;             // File system
const heap = std.heap;         // Allocators
const ArrayList = std.ArrayList;

ArrayList

var list = std.ArrayList(i32).init(allocator);
defer list.deinit();

try list.append(42);
try list.appendSlice(&.{ 1, 2, 3 });

for (list.items) |item| {
    std.debug.print("{}\n", .{item});
}

HashMap

var map = std.StringHashMap(i32).init(allocator);
defer map.deinit();

try map.put("answer", 42);

if (map.get("answer")) |value| {
    std.debug.print("Found: {}\n", .{value});
}

File I/O

// Read entire file
const data = try std.fs.cwd().readFileAlloc(
    allocator,
    "file.txt",
    1024 * 1024,  // max size
);
defer allocator.free(data);

// Write file
const file = try std.fs.cwd().createFile("out.txt", .{});
defer file.close();
try file.writeAll("Hello!");

Strings

// Strings are []const u8
const str: []const u8 = "hello";

// String comparison
const equal = std.mem.eql(u8, str, "hello");

// Split string
var iter = std.mem.splitScalar(u8, "a,b,c", ',');
while (iter.next()) |part| {
    std.debug.print("{s}\n", .{part});
}

// Join strings
const parts = [_][]const u8{ "hello", "world" };
const joined = try std.mem.join(allocator, " ", &parts);

// Format to buffer
var buf: [100]u8 = undefined;
const result = try std.fmt.bufPrint(&buf, "Value: {d}", .{42});

Sorting & Searching

// Sort slice
var items = [_]i32{ 5, 2, 8, 1, 9 };
std.mem.sort(i32, &items, {}, std.sort.asc(i32));

// Binary search (requires sorted)
const idx = std.sort.binarySearch(
    i32,
    &items,
    8,
    {},
    struct {
        fn cmp(_: void, a: i32, b: i32) std.math.Order {
            return std.math.order(a, b);
        }
    }.cmp,
);

Random

var prng = std.Random.DefaultPrng.init(0);
const rand = prng.random();

const n = rand.int(u32);              // random u32
const bounded = rand.intRangeAtMost(u8, 1, 100);  // 1-100
const f = rand.float(f32);            // 0.0 to 1.0
rand.shuffle(i32, &items);            // shuffle slice

Testing

Zig has built-in testing. Tests are written inline with code and run with zig test.

Writing Tests

const std = @import("std");
const expect = std.testing.expect;

fn add(a: i32, b: i32) i32 {
    return a + b;
}

// Test block
test "basic add" {
    const result = add(2, 3);
    try expect(result == 5);
}

// Test with error handling
test "expect equal" {
    try std.testing.expectEqual(5, add(2, 3));
}

// Test that function returns error
test "expect error" {
    const result = mayFail(false);
    try std.testing.expectError(error.Failed, result);
}

Test Assertions

// Basic expectation
try std.testing.expect(x == 42);

// Equality check (shows both values on failure)
try std.testing.expectEqual(42, x);

// Approximate float equality
try std.testing.expectApproxEqAbs(3.14, pi, 0.01);

// String/slice equality
try std.testing.expectEqualStrings("hello", str);
try std.testing.expectEqualSlices(u8, expected, actual);

// Check for error
try std.testing.expectError(error.OutOfMemory, result);

// Format failure message
try std.testing.expectFmt("42", "{d}", .{x});

Running Tests

# Run all tests in file
zig test src/main.zig

# Run specific test by name filter
zig test src/main.zig --test-filter "basic add"

# Run with test allocator (detects leaks)
zig test src/main.zig  # uses testing allocator by default

# In build.zig
const tests = b.addTest(.{
    .root_source_file = b.path("src/main.zig"),
});
const test_step = b.step("test", "Run tests");
test_step.dependOn(&b.addRunArtifact(tests).step);

Test Allocator

test "memory test" {
    // Testing allocator detects leaks and use-after-free
    const allocator = std.testing.allocator;

    const slice = try allocator.alloc(u8, 100);
    defer allocator.free(slice);  // must free or test fails

    // ... use slice ...
}
The testing allocator automatically fails tests that leak memory. Use it to ensure your code doesn't have memory leaks.

C Interoperability

Zig can seamlessly import and use C code. It can also export Zig functions for use from C.

Import C Headers

// Import C standard library
const c = @cImport({
    @cInclude("stdio.h");
    @cInclude("stdlib.h");
});

pub fn main() void {
    _ = c.printf("Hello from C!\n");

    const ptr = c.malloc(100);
    defer c.free(ptr);
}

Link C Libraries

// build.zig - link system C library
const exe = b.addExecutable(.{
    .name = "myapp",
    .root_source_file = b.path("src/main.zig"),
});

// Link libc
exe.linkLibC();

// Link system library
exe.linkSystemLibrary("curl");
exe.linkSystemLibrary("sqlite3");

// Add include path
exe.addIncludePath(b.path("include"));

Export to C

// Export function callable from C
export fn zig_add(a: c_int, b: c_int) c_int {
    return a + b;
}

// Export with custom name
comptime {
    @export(zigFunction, .{ .name = "c_function_name" });
}

C Types

// C-compatible integer types
c_short       c_ushort
c_int         c_uint
c_long        c_ulong
c_longlong    c_ulonglong

// Pointers
[*c]T         // C pointer (nullable, no bounds)
?[*]T         // Optional many-pointer

// Convert C string to Zig slice
const c_str: [*:0]const u8 = "hello";
const zig_str = std.mem.span(c_str);  // []const u8

Calling Conventions

// Use C calling convention for interop
fn callback(data: ?*anyopaque) callconv(.C) void {
    // Can be passed to C functions expecting function pointers
}

// Extern function declaration
extern "c" fn some_c_function(arg: c_int) c_int;
Zig can compile C code directly. Add .c files to your build with exe.addCSourceFile().

Build System

Quick Commands

# Run directly
zig run main.zig

# Build executable
zig build-exe main.zig

# Build with optimizations
zig build-exe -O ReleaseFast main.zig

# Run tests
zig test test.zig

# Initialize project
zig init

build.zig (Minimal)

const std = @import("std");

pub fn build(b: *std.Build) void {
    const exe = b.addExecutable(.{
        .name = "myapp",
        .root_source_file = b.path("src/main.zig"),
        .target = b.standardTargetOptions(.{}),
        .optimize = b.standardOptimizeOption(.{}),
    });

    b.installArtifact(exe);

    const run_cmd = b.addRunArtifact(exe);
    const run_step = b.step("run", "Run the app");
    run_step.dependOn(&run_cmd.step);
}

Cross-Compilation

# Build for Windows from Linux/macOS
zig build -Dtarget=x86_64-windows

# Build for Linux from anywhere
zig build -Dtarget=x86_64-linux-gnu

# Build for macOS
zig build -Dtarget=aarch64-macos