← 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!");

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