Skip to content
Zig logo

Zig

First appeared 2016 · Andrew Kelley

A C replacement with no hidden control flow, no hidden allocations and no preprocessor.

Overview

Zig is a systems programming language designed as a modern replacement for C, with no hidden control flow, no hidden memory allocations and no preprocessor. Every allocation goes through an explicit allocator parameter, which makes memory strategy visible in function signatures and testable — the standard library ships a testing allocator that fails your tests if they leak. Errors are values in a dedicated error union type, checked by the compiler, with no exceptions and no unwinding. Its comptime feature replaces macros, templates and generics with ordinary Zig code that runs at compile time, meaning one mechanism covers what three separate systems handle in C++. Zig also ships an exceptional C toolchain: it compiles C, cross-compiles to dozens of targets out of the box, and can be dropped into existing C projects incrementally. The language is not yet at 1.0 and its API still changes between releases.

Key facts

The reference details, without the paragraph.

First appeared
2016
Designed by
Andrew Kelley
Typing
Static and strong, with compile-time duck typing through comptime
Execution
Compiled ahead of time to native code; self-hosted backend plus LLVM
Memory model
Manual, but allocators are explicit parameters rather than global state
Package manager
Built into the compiler via build.zig.zon
File extensions
.zig
Current status
Pre-1.0 — breaking changes between minor releases are normal
Notable extra
Ships a complete C and C++ cross-compiler out of the box
Licence
MIT

History

How the language got here — the decisions that still shape how you write it.

Zig was created by Andrew Kelley, who began work in 2015 out of frustration with C's accumulated problems and with C++'s answer to them. His diagnosis was that C's failings were not primarily about memory safety but about hidden behaviour: implicit conversions, a preprocessor that operates on text rather than code, undefined behaviour that the optimiser exploits silently, and build systems that are separate languages in their own right. Zig's design responds to each of these directly, and its guiding principle — that reading code should tell you what it does — is enforced by removing operator overloading, implicit conversions and hidden allocations. Development has been funded since 2020 by the Zig Software Foundation, a non-profit relying on donations rather than a corporate sponsor, which the project regards as important to its independence. Zig's most visible early adoption has come from an unexpected direction: because its compiler is also a first-rate C and C++ cross-compiler, projects such as Bun and Uber's build infrastructure adopted Zig as a toolchain before writing any Zig code. The language remains pre-1.0 and breaking changes are frequent.

  1. 2015

    Started out of frustration with C

    Andrew Kelley begins Zig, arguing that C's real problem is hidden behaviour — implicit conversions, a text-substituting preprocessor, and undefined behaviour the optimiser silently exploits.

  2. 2017

    comptime replaces macros and generics

    Arbitrary Zig code running at compile time subsumes what C++ splits across templates, constexpr and the preprocessor. One mechanism covers three.

  3. 2020

    The Zig Software Foundation

    A non-profit is formed to fund development from donations rather than a corporate sponsor, which the project treats as important to its independence.

  4. 2021

    Adopted as a C toolchain first

    Uber and others start using `zig cc` for painless C cross-compilation, long before writing any Zig. The compiler's toolchain quality becomes an unexpected adoption route.

  5. 2022

    Bun ships

    A JavaScript runtime written in Zig gains substantial attention and becomes the language's most visible production use.

  6. 2023–2024

    Self-hosted compiler and a package manager

    Zig removes its dependency on LLVM for the default path and gains a built-in package manager, reducing build times and external dependencies.

  7. 2025

    Still pre-1.0

    The language continues to make breaking changes deliberately, prioritising getting the design right over API stability. Production users pin exact compiler versions.

What it is good at

The reasons teams pick it, stated concretely.

  • Nothing happens that you cannot see

    No operator overloading, no implicit conversions, no hidden allocations, no exceptions unwinding through your code. Reading a Zig function tells you what it does, which is the language's central design commitment.

  • Allocators are explicit and testable

    Any function that allocates takes an allocator parameter, so memory strategy is visible in the signature. The standard testing allocator fails a test that leaks — memory correctness becomes something you can assert on.

  • comptime is remarkably elegant

    Generics, compile-time computation, reflection and code generation are all just Zig code that runs during compilation. No template metaprogramming language, no macro system, no separate syntax to learn.

  • The best C toolchain going

    `zig cc` compiles C and cross-compiles to dozens of targets with no additional setup, bundling libc for each. Many projects adopt Zig purely as a build tool before writing a line of the language.

  • Errors are values the compiler checks

    Error unions make every failure path explicit, `try` propagates concisely, and ignoring an error is a compile error rather than a convention. No exceptions, no unwinding, no hidden control flow.

Trade-offs

Every language costs you something. Knowing what, before you commit, is the whole point.

  • Pre-1.0, and it means it

    Minor releases break code, standard library APIs are renamed, and tutorials go stale quickly. Production users pin an exact compiler version and budget time for upgrades.

  • No memory safety guarantees

    Zig catches more than C — bounds checking in safe builds, defined integer overflow behaviour, no null pointers — but it does not prevent use-after-free. If memory safety is the goal, Rust offers guarantees Zig does not.

  • A small ecosystem

    The package manager is young and the library selection thin. You will write more yourself, or bind to C libraries — which Zig makes unusually easy, but it is still work.

  • Sparse learning material

    The official documentation is improving but incomplete, and much community material targets outdated versions. Reading the standard library source is a normal part of learning Zig.

  • Manual memory management is still manual

    Explicit allocators make ownership visible but not automatic. Every allocation still needs a matching free on every path, and `defer` helps rather than solves.

Code examples

Not syntax tours — the idioms that make code read like the language rather than a translation of another one.

Explicit allocators and defer
const std = @import("std");

// The allocator is a parameter — the caller decides the strategy.
fn readLines(allocator: std.mem.Allocator, path: []const u8) ![][]u8 {
    const file = try std.fs.cwd().openFile(path, .{});
    defer file.close();              // runs on every exit path

    var lines = std.ArrayList([]u8).init(allocator);
    errdefer lines.deinit();         // runs only if we return an error

    var buf: [4096]u8 = undefined;
    var reader = file.reader();
    while (try reader.readUntilDelimiterOrEof(&buf, '\n')) |line| {
        try lines.append(try allocator.dupe(u8, line));
    }
    return lines.toOwnedSlice();
}

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

    const lines = try readLines(allocator, "books.txt");
    defer allocator.free(lines);
}
`defer` runs at scope exit; `errdefer` runs only on the error path. Together they make cleanup local to acquisition without needing destructors — and the GeneralPurposeAllocator reports any leak when it deinits.
Errors as values, checked by the compiler
const ConfigError = error{
    FileNotFound,
    InvalidFormat,
    MissingKey,
};

// The ! means "this returns Config or one of the errors in the set".
fn loadConfig(allocator: std.mem.Allocator, path: []const u8) !Config {
    const contents = std.fs.cwd().readFileAlloc(allocator, path, 1024 * 1024) catch |err| {
        return switch (err) {
            error.FileNotFound => ConfigError.FileNotFound,
            else => err,
        };
    };
    defer allocator.free(contents);

    return parseConfig(contents) orelse ConfigError.InvalidFormat;
}

// try is shorthand for `catch |err| return err`.
const config = try loadConfig(allocator, "app.conf");

// Or handle it locally.
const config2 = loadConfig(allocator, "app.conf") catch |err| switch (err) {
    ConfigError.FileNotFound => Config.default(),
    else => return err,
};
The error set is part of the type, so the compiler knows exactly what can fail and rejects a switch that misses a case. Ignoring the error entirely will not compile — there is no equivalent of silently dropping a return value.
comptime — generics without a separate language
// A generic function: the type is an ordinary compile-time parameter.
fn Stack(comptime T: type) type {
    return struct {
        items: []T,
        len: usize = 0,
        allocator: std.mem.Allocator,

        const Self = @This();

        pub fn init(allocator: std.mem.Allocator) !Self {
            return .{ .items = try allocator.alloc(T, 8), .allocator = allocator };
        }

        pub fn push(self: *Self, value: T) !void {
            if (self.len == self.items.len) {
                self.items = try self.allocator.realloc(self.items, self.items.len * 2);
            }
            self.items[self.len] = value;
            self.len += 1;
        }
    };
}

var stack = try Stack(u32).init(allocator);

// Reflection is also just comptime code.
fn describe(value: anytype) void {
    inline for (@typeInfo(@TypeOf(value)).Struct.fields) |field| {
        std.debug.print("{s}: {any}\n", .{ field.name, @field(value, field.name) });
    }
}
A generic type is a function returning a type, evaluated at compile time. There is no template syntax, no separate constexpr sublanguage and no macros — the same Zig you already know, run earlier.
Tests, and calling C directly
// Tests live next to the code and run with `zig test`.
test "stack grows correctly" {
    // This allocator fails the test if anything leaks.
    var stack = try Stack(u32).init(std.testing.allocator);
    defer stack.deinit();

    try stack.push(1);
    try stack.push(2);
    try std.testing.expectEqual(@as(usize, 2), stack.len);
}

// C interop needs no bindings or generated glue.
const c = @cImport({
    @cInclude("sqlite3.h");
});

pub fn openDb(path: [*:0]const u8) !*c.sqlite3 {
    var db: ?*c.sqlite3 = null;
    if (c.sqlite3_open(path, &db) != c.SQLITE_OK) {
        return error.DatabaseOpenFailed;
    }
    return db.?;
}
`std.testing.allocator` turning a leak into a test failure is genuinely distinctive — memory correctness becomes a normal assertion rather than something you check with an external tool. `@cImport` parses the C header directly at compile time.

Common pitfalls

The mistakes that cost everyone an afternoon at least once.

  • Forgetting to free on an error path

    `defer` covers normal returns, but if a later step fails you may want to undo an earlier allocation. That is what `errdefer` is for — it runs only when the function returns an error.

  • Returning a slice into a stack buffer

    The buffer is gone when the function returns, leaving a slice into invalid memory. Allocate with the caller's allocator, or take a caller-provided buffer.

  • Confusing arrays, slices and pointers

    `[5]u8` is an array by value, `[]u8` is a slice with a length, and `[*]u8` is a bare pointer with none. They are distinct types and the errors are confusing until the distinction clicks.

  • Assuming code from a tutorial still compiles

    Zig is pre-1.0 and the standard library is renamed regularly. Check which compiler version an example targets — most breakage is a renamed function, not a conceptual change.

  • Integer overflow panics in safe builds

    Unlike C, `+` traps on overflow in Debug and ReleaseSafe. Use `+%` for explicit wrapping arithmetic rather than treating the panic as a bug.

  • Leaving `undefined` values unset

    `undefined` really is undefined memory, not a null. Reading it before assignment is undefined behaviour — safe builds fill it with `0xaa` to make the mistake obvious.

In production

Where it is running at scale, and what it is doing there.

  • Bun

    The JavaScript runtime and toolkit is written in Zig.

  • TigerBeetle

    A financial accounting database built in Zig for deterministic performance.

  • Uber

    Adopted the Zig compiler as a C/C++ cross-compilation toolchain.

  • Ghostty

    A GPU-accelerated terminal emulator written in Zig.

Learning path

A realistic order to learn things in, with something to build at each step.

  1. 1

    Week 1

    Syntax and the memory model

    Values and types, slices versus arrays, optionals, structs, and above all allocators — why they are parameters and what the different standard ones do. Work through Ziglings.

    Build this: Write a program that reads a file and counts words, passing an allocator explicitly throughout.

  2. 2

    Week 2

    Errors and cleanup

    Error sets and error unions, `try`, `catch`, `defer` and `errdefer`. Getting cleanup right on every path is the core discipline the language asks of you.

    Build this: Add proper error handling and leak-free cleanup, and verify it with the testing allocator.

  3. 3

    Weeks 3–4

    comptime

    Compile-time parameters, generic types, `inline for`, `@typeInfo` reflection and compile-time assertions. This is where Zig stops looking like a simpler C.

    Build this: Write a generic container and a compile-time reflection helper that prints any struct.

  4. 4

    Months 2–3

    Real programs and C interop

    build.zig and the package manager, `@cImport` for C libraries, cross-compilation targets, and reading the standard library — which is the most reliable documentation available.

    Build this: Bind a C library and cross-compile the result for three platforms from one machine.

  5. 5

    Ongoing

    Systems work

    Custom allocators including arenas and fixed buffers, `async` where available, freestanding and embedded targets, and reading generated assembly when performance matters.

    Build this: Replace a general-purpose allocator with an arena in a hot path and measure the difference.

Ecosystem and tooling

The tools you will end up installing whichever project you join.

ToolWhat it does
zig buildBuild system, package manager, test runner and cross-compiler, all in the compiler binary
zig ccA drop-in C and C++ compiler with effortless cross-compilation — widely used without writing Zig
stdThe standard library; also the most complete documentation, since reading it is normal practice
ZLSThe Zig Language Server, providing completion and diagnostics in editors
ZiglingsA series of small broken programs to fix — the most effective way to learn the language
zap / http.zigHTTP server libraries, illustrating both the promise and the youth of the ecosystem
machA game engine and graphics toolkit, one of the larger Zig projects
BunThe JavaScript runtime written in Zig — the language's flagship production use

Zig libraries

Library coverage for Zig is on the way.

The guide above is complete. In the meantime, the catalogues for Python, Java, JavaScript, C and C++ are fully written.

Browse all libraries

Frequently asked

Is Zig ready for production?

Some people run it in production — Bun and TigerBeetle are real systems — but it is pre-1.0 and means it. Minor releases break code and standard library APIs are renamed. If you adopt it, pin an exact compiler version and expect to spend time on upgrades.

Zig or Rust?

Rust if memory safety guarantees matter most, or you want a mature ecosystem and better hiring prospects. Zig if you want C-like simplicity, explicit control over allocation, faster compilation and a far gentler learning curve. Zig does not prevent use-after-free; Rust does. That difference should usually decide it.

Why do people use Zig without writing Zig?

Because `zig cc` is an outstanding C and C++ cross-compiler. It bundles libc for dozens of targets, so you can build Linux binaries from macOS with one flag and no toolchain setup. Several large projects adopted it purely for this.

What makes comptime different from C++ templates?

It is the same language, run earlier. There is no separate template syntax, no SFINAE, no constexpr sublanguage and no macro preprocessor — just Zig code with `comptime` parameters. The error messages are correspondingly ordinary rather than pages of instantiation trace.

Does Zig have garbage collection?

No. Memory is manual, but allocators are explicit parameters rather than a global `malloc`, which makes strategy visible and testable. It also enables arena and fixed-buffer allocation patterns that are awkward in C.

Is the ecosystem large enough to build something real?

For systems-level work where you would mostly write your own code and bind to C libraries, yes — and Zig's C interop is exceptional. For anything expecting a rich library ecosystem, no. That gap is the main practical reason to wait.