⚡ Zig Guide LiveUnofficial
✓ Zig 0.17.0-dev.1454+5faa79730On an older Zig?

Coming from an Older Zig

This guide targets Zig master: the version in the footer is the compiler that actually compiled and ran every snippet here. If you are on a tagged release, some of this code will not build for you.

Rather than maintaining a parallel guide per release, this page gives you the delta: what changed, what it looks like now, and which chapter covers it. That is usually what you actually need.

At a glance

WhatBeforeNow
main signaturepub fn main() !voidpub fn main(init: std.process.Init) !void
Writersunbuffered, no flushbuffered, must flush
File APIstd.fs.Filestd.Io.File, and takes an Io
Array repeat"-" ** 5@splat (** is gone)
ArrayListinit(allocator).empty, allocator per call
Mutexstd.Thread.Mutexstd.Io.Mutex, lock(io)
Custom formatfour parameters, used by {}one writer parameter, used by {f}
Enum reflectioninfo.fields[i].nameinfo.field_names[i]
addExecutablesource file directlyroot_module

main takes an argument

The one that breaks the very first program you write.

// before
pub fn main() !void {
    std.debug.print("Hello\n", .{});
}
// now
pub fn main(init: std.process.Init) !void {
    try std.Io.File.stdout().writeStreamingAll(init.io, "Hello\n");
}

init.io is an Io instance: the same idea as Allocator, applied to anything that can block. There is no global stdout; you must be handed the capability to write.

Hello World

Writers are buffered (“writergate”, 0.15)

// before: appeared immediately
const stdout = std.io.getStdOut().writer();
try stdout.print("{d}\n", .{42});
// now: nothing is written until you flush
var buf: [1024]u8 = undefined;
var file_writer = std.Io.File.stdout().writerStreaming(io, &buf);
const out = &file_writer.interface;
try out.print("{d}\n", .{42});
try out.flush();          // ← forget this and you get no output at all

The buffer belongs to the interface, which is why you supply it: no hidden allocation. The missing flush is the single most common symptom of following an older tutorial.

Readers and Writers

Filesystem moved onto Io (0.16)

// before
var file = try std.fs.cwd().openFile("data.txt", .{});
defer file.close();
// now
var dir = std.Io.Dir.cwd();
const file = try dir.openFile(io, "data.txt", .{});
defer file.close(io);

Uniform rule: anything that touches the disk takes the io instance.

Filesystem

** no longer exists

// before
const dashes = "-" ** 5;
// now
const dashes: [5]u8 = @splat('-');

Worth calling out because the failure is actively misleading: ** no longer tokenises at all, so the compiler reports “binary operator * has whitespace on one side, but not the other”, which tells you nothing about what actually happened. ++ concatenation is unaffected.

Arrays

ArrayList and hash maps are unmanaged

// before: the list stored your allocator
var list = std.ArrayList(u8).init(allocator);
defer list.deinit();
try list.append('a');
// now: you pass it to anything that can allocate
var list: std.ArrayList(u8) = .empty;
defer list.deinit(gpa);
try list.append(gpa, 'a');

More typing, but the allocator is visible at every call site that can fail. The same change applies to StringHashMapUnmanaged and AutoHashMapUnmanaged.

ArrayList · Hash Maps

Synchronisation moved to Io

// before
var mutex: std.Thread.Mutex = .{};
mutex.lock();
defer mutex.unlock();
// now
var mutex: std.Io.Mutex = .init;
try mutex.lock(io);
defer mutex.unlock(io);

lock can fail only because it can be cancelled. std.Thread.spawn still exists, but structured concurrency via io.async and Io.Group is the better default, and works on targets with no threads at all.

Threads · Concurrency

Custom format methods

// before
pub fn format(
    self: Point,
    comptime fmt: []const u8,
    options: std.fmt.FormatOptions,
    writer: anytype,
) !void { ... }
// now
pub fn format(self: Point, writer: *std.Io.Writer) std.Io.Writer.Error!void {
    try writer.print("({d}, {d})", .{ self.x, self.y });
}

And it is no longer invoked by {}; you must ask for it with {f}. {t} prints an enum tag name or error name.

Advanced Formatting

Enum reflection

// before
const fields = @typeInfo(Direction).@"enum".fields;
fields[2].name;
// now: parallel arrays
const info = @typeInfo(Direction).@"enum";
info.field_names[2];
info.field_values[2];

This one is recent: it landed between 0.17.0-dev.644 and dev.1441, and was the only break across all 53 snippets in this guide when the compiler was updated across roughly 800 commits.

Enums

build.zig

// before
const exe = b.addExecutable(.{
    .name = "app",
    .root_source_file = b.path("src/main.zig"),
    .target = target,
    .optimize = optimize,
});
// now: build the module first
const exe = b.addExecutable(.{
    .name = "app",
    .root_module = b.createModule(.{
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    }),
});

Zig Build


If you are staying on a release

Nothing here is a reason to upgrade in a hurry. A tagged release is the right choice for most work; master is what this guide tracks because a guide that lags is a guide that quietly lies.

Use this page as a translation layer: read the chapter for the concept, then map the syntax back. The ideas (explicit allocators, errors as values, exhaustive switches, defer, comptime) have not changed at all, and those are the parts worth your attention.