A language tour

The Directness of Zig

No hidden control flow. No hidden allocations. No preprocessor. If it happens, you wrote it.

scroll

01 — No Hidden Control Flow

What you see is what runs

Zig's central promise: reading code tells you what the machine does. No operator overloading, no implicit conversions, no destructors running behind your back. Every allocation is explicit; every branch is visible.

"Zig is a general-purpose programming language and toolchain for maintaining robust, optimal and reusable software."

— ziglang.org
explicit.zig
const std = @import("std");

pub fn main() void {
    // No implicit integer coercion — you must be explicit
    const x: u32 = 42;
    const y: u64 = x;  // widening is fine
    const z: u32 = @intCast(y);  // narrowing requires explicit cast

    // Integer overflow is a compile error in debug, checked in safe,
    // and wrapping only when you say so explicitly
    const max: u8 = 255;
    const wrapped = max +% 1;  // +% means "wrapping add" — explicit intent

    std.debug.print("{} {} {}\n", .{ x, z, wrapped });
}

+% is a wrapping addition operator. +| is saturating addition. You choose the overflow behaviour at the call site — the default is "trap in debug, undefined in release."


02 — Comptime

Generics without a special syntax

Zig has no generics syntax, no templates, no macros. Instead it has comptime — the ability to run arbitrary Zig code at compile time. Types are values. Functions that take types are generic functions. The language doesn't need a second language.

comptime.zig
const std = @import("std");

// Types are values at comptime — this IS the generics system
fn Stack(comptime T: type) type {
    return struct {
        items: std.ArrayList(T),

        pub fn push(self: *@This(), val: T) !void {
            try self.items.append(val);
        }
    };
}

// Compute fibonacci at compile time — zero runtime cost
fn fib(comptime n: u32) u64 {
    if (n < 2) return n;
    return fib(n - 1) + fib(n - 2);
}

const fib40: u64 = fib(40);  // evaluated entirely at compile time

Stack(i32) and Stack(f64) are two different types, generated by running the same function at compile time with different arguments. No template specialisation syntax required.


03 — Error Handling

Errors are values

Zig has no exceptions. Errors are part of the return type — the ! prefix marks a function that can fail. The try keyword propagates errors up the call stack. You can't accidentally ignore an error.

errors.zig
const std = @import("std");

const ParseError = error{
    InvalidCharacter,
    Overflow,
    Empty,
};

fn parsePositive(s: []const u8) ParseError!u32 {
    if (s.len == 0) return error.Empty;
    return std.fmt.parseInt(u32, s, 10) catch error.InvalidCharacter;
}

pub fn main() !void {
    // try propagates error upward — like ? in Rust
    const n = try parsePositive("42");

    // catch handles it inline
    const m = parsePositive("abc") catch |err| {
        std.debug.print("Failed: {}\n", .{err});
        return;
    };
}

The return type ParseError!u32 is an error union — a value that is either a ParseError or a u32. The compiler enforces that you handle both cases.


04 — Explicit Allocators

Every allocation is visible

In Zig, allocators are passed explicitly to anything that needs heap memory. There's no global allocator you accidentally use. This makes it trivial to swap a general-purpose allocator for an arena, a pool, or a fixed buffer.

allocators.zig
const std = @import("std");

pub fn main() !void {
    // General-purpose allocator with leak detection
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();  // defer = run at scope exit
    const alloc = gpa.allocator();

    // ArrayList knows nothing about memory — you give it an allocator
    var list = std.ArrayList(u32).init(alloc);
    defer list.deinit();

    try list.append(1);
    try list.append(2);

    // Swap in an arena allocator for a phase: O(1) free-all
    var arena = std.heap.ArenaAllocator.init(alloc);
    defer arena.deinit();
}

defer runs a statement when the current scope exits — whether by normal return or error. Combined with explicit allocators, it makes resource cleanup deterministic and obvious.


05 — Cross-Compilation

One toolchain to rule them all

Zig ships with a complete C and C++ cross-compilation toolchain. Without installing anything else, you can compile for any target Zig supports — x86_64, ARM, WASM, RISC-V. Even C projects use zig cc as a drop-in cross-compiler.

build.zig
const std = @import("std");

// Build script is Zig code — no DSL, no YAML
pub fn build(b: *std.Build) void {
    const targets = .{
        "x86_64-linux",
        "aarch64-macos",
        "wasm32-wasi",
    };

    inline for (targets) |target_str| {
        const target = b.resolveTargetQuery(
            try std.Target.Query.parse(.{ .arch_os_abi = target_str }),
        );
        const exe = b.addExecutable(.{
            .name = "myapp-" ++ target_str,
            .root_source_file = b.path("src/main.zig"),
            .target = target,
        });
        b.installArtifact(exe);
    }
}

The build system is just Zig code — inline for unrolls the loop at compile time, generating three build steps from a single loop body. No Makefile, no CMake, no shell scripts.


06 — The Whole Picture

Why Zig is worth watching

🔍

No Undefined Behaviour (by default)

In debug mode, Zig detects integer overflow, out-of-bounds, and null dereference at runtime — no UB silent corruption.

🧪

Built-in Testing

test "name" { ... } blocks are first-class. zig test runs them all. No external framework needed.

🔗

C Interop

@cImport reads C headers directly. Call any C library with zero boilerplate — Zig and C speak the same ABI.

📦

No Hidden Dependencies

Zig links against no libc by default. Your binary is exactly what you put in it — and nothing else.

🏗️

Replace make & cmake

zig cc is a drop-in C cross-compiler. Many C and C++ projects now use Zig just for building.

🌱

Still Young

Zig is pre-1.0. The best time to watch a language find its footing — and the design is already remarkable.