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.
- 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.
- 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.
- 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.
- 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.
- 2022
Bun ships
A JavaScript runtime written in Zig gains substantial attention and becomes the language's most visible production use.
- 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.
- 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.
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);
}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,
};// 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) });
}
}// 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.?;
}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
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
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
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
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
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.
| Tool | Category | What it does |
|---|---|---|
| zig build | Toolchain | Build system, package manager, test runner and cross-compiler, all in the compiler binary |
| zig cc | Toolchain | A drop-in C and C++ compiler with effortless cross-compilation — widely used without writing Zig |
| std | Core | The standard library; also the most complete documentation, since reading it is normal practice |
| ZLS | Tooling | The Zig Language Server, providing completion and diagnostics in editors |
| Ziglings | Learning | A series of small broken programs to fix — the most effective way to learn the language |
| zap / http.zig | Web | HTTP server libraries, illustrating both the promise and the youth of the ecosystem |
| mach | Graphics | A game engine and graphics toolkit, one of the larger Zig projects |
| Bun | Showcase | The 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 librariesFrequently 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.




