Rust won the systems programming conversation. Memory safety without a garbage collector, a package ecosystem that actually works, and adoption at Microsoft, Google, the Linux kernel, and the Rust Foundation. The debate is over.

But a new language is building real momentum, and it takes a fundamentally different philosophy to the same problem space. Zig is worth understanding.

What Zig Is

Zig is a general-purpose systems programming language created by Andrew Kelley, first released in 2016 and still pre-1.0. It targets the same space as C - low-level systems code, embedded software, performance-critical applications - but with a cleaner model for memory management, error handling, and compile-time programming.

The stated goals: no hidden control flow, no hidden allocations, no macros, no preprocessor. What you see is what executes.

Explicit Allocators

In C, functions can allocate memory anywhere without the caller knowing. In Rust, the allocator is implicit. In Zig, every function that allocates takes an allocator as a parameter.

const std = @import("std");
const Allocator = std.mem.Allocator;

fn buildUserList(allocator: Allocator, count: usize) ![]User {
    var list = try std.ArrayList(User).initCapacity(allocator, count);
    defer list.deinit();

    for (0..count) |i| {
        try list.append(User{ .id = i });
    }

    return try list.toOwnedSlice();
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    const users = try buildUserList(allocator, 100);
    defer allocator.free(users);
}

The allocator is explicit at every call site. In tests, use a testing allocator that detects leaks. In production, use the general-purpose allocator. For embedded systems, use a fixed-buffer allocator. The same code works with any allocator - no code changes, just pass a different one.

This makes memory behavior auditable. You can grep for .alloc and find every allocation site.

comptime: Compile-Time Execution

Zig’s comptime replaces macros, templates, and generics with one mechanism: code that runs at compile time.

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

// Compiler generates separate implementations for each type
const int_max = max(i32, 10, 20);
const float_max = max(f64, 3.14, 2.71);

comptime T: type means T is known at compile time. The function is generic but the implementation is explicit. No template metaprogramming magic, no macro system, no procedural macros. The same construct handles all of them.

// Conditional compilation
fn platformSpecific() void {
    if (comptime builtin.os.tag == .linux) {
        // Linux path
    } else if (comptime builtin.os.tag == .macos) {
        // macOS path
    }
}

The if (comptime ...) branch is eliminated at compile time. The binary only contains the code for the target platform.

Error Handling Without Exceptions

Zig uses error unions - the return type encodes both success and error.

const FileError = error{
    NotFound,
    PermissionDenied,
    InvalidFormat,
};

fn readConfig(path: []const u8) FileError!Config {
    const file = try std.fs.openFileAbsolute(path, .{});
    defer file.close();
    // try propagates errors up, equivalent to Rust's ?
    const content = try file.readToEndAlloc(allocator, 1024 * 1024);
    return parseConfig(content);
}

try is syntactic sugar for “if this returns an error, return that error from this function.” The compiler enforces error handling - you cannot ignore an error union return. No silent failures, no unchecked exceptions.

Real Production Uses

Zig is pre-1.0 but is in production in some notable places:

Bun - the JavaScript runtime that competes with Node.js and Deno is written in Zig. It is genuinely faster than Node at most benchmarks. Bun’s HTTP server, file I/O, and JavaScript engine integration are all Zig.

TigerBeetle - a financial transactions database designed for correctness and performance is written in Zig. They use it for the guarantees around memory allocation and the deterministic simulation testing that Zig’s explicit allocator model enables.

Mach - a game engine and graphics framework built in Zig.

Zig vs Rust: Different Tradeoffs

Aspect Zig Rust
Memory safety Manual (but auditable) Compile-time enforced
Borrow checker No Yes
Learning curve Moderate High
Compile times Fast Slow
C interop Trivially easy Requires FFI
Ecosystem maturity Early Strong
Production adoption Limited Widespread

Rust gives you memory safety guarantees enforced by the compiler. Zig gives you explicit control with no hidden behavior. For code that must be formally verified or where undefined behavior is unacceptable, Rust’s guarantees matter. For systems code where you want maximum control and auditability, Zig’s model is appealing.

Zig’s C interoperability is notably better - you can directly include C headers and call C functions without any binding code. This makes adopting Zig incrementally in existing C projects realistic.

Bottom Line

Zig is not competing with Rust on the same dimensions. Rust optimizes for memory safety guarantees. Zig optimizes for simplicity, explicit control, and C compatibility. The language is not production-ready for most use cases at 0.12 - but the projects that are using it in production demonstrate what it can do. Watch Zig reach 1.0 and watch whether its approach to allocators and comptime influences other language designs. The ideas are good enough to matter regardless of whether Zig itself takes off.