Zig
Zig is a general-purpose systems language built around a single organizing principle: no hidden control flow, no hidden allocations. If a line of code can jump somewhere else, you can see it (try, catch, return). If a function needs memory, it takes an allocator as a parameter. There are no destructors that run implicitly, no operator overloading, no exceptions unwinding through your call stack, and no garbage collector.
The result reads like a C that learned from the last fifty years: real error handling, slices that carry their length, tagged unions with exhaustive switching, compile-time code execution that replaces both macros and generics, and a build system that cross-compiles to virtually any target out of the box. Zig is also, unusually, a drop-in C and C++ compiler — which is how many teams meet it, long before they write a line of Zig.
⚠️ Warning: Zig is pre-1.0. The language and standard library still change between releases, sometimes substantially. It is used in production by teams who accept that cost (TigerBeetle, Bun); it is not yet a conservative choice.
TL;DR
- No hidden control flow, no hidden allocations — every jump and every allocation is visible in the source.
- Allocators are parameters. The standard library never allocates behind your back; you choose and pass the allocator.
comptimeruns ordinary Zig at compile time, and it replaces macros, generics, and most reflection with one mechanism.- Error unions (
!T) plustry,catch, anderrdefergive exhaustive, allocation-free, zero-hidden-cost error handling. defer/errdeferput cleanup next to acquisition — the answer to C'sgoto cleanupladders.- Best-in-class C interop:
@cImporta header and call it, no bindings, no FFI layer. zig ccis a cross-compiling C/C++ toolchain that many projects adopt without writing Zig at all.- Pre-1.0: breaking changes between versions, a young ecosystem, and a small talent pool.
Quick Example
Explicit allocation, error handling, and deterministic cleanup:
Nothing here allocates unless you passed an allocator to it, and nothing runs at scope exit unless you wrote defer.
Core Concepts
Allocators are explicit
There is no global heap in Zig's standard library. Any function that needs memory takes an std.mem.Allocator. This makes memory strategy a caller's decision, and makes "does this library allocate?" answerable by reading its signatures.
The arena pattern is the one that changes how you write code. Allocate freely, free once:
Errors are values in a union
!T means "a T, or one of an inferred error set." Errors are not exceptions — no unwinding, no allocation, no hidden control flow. Propagation is spelled try.
Errors have no payload — they're just tags. Attach context by returning a struct or setting a diagnostic out-parameter. Unlike Go's if err != nil boilerplate, try is a single token; unlike exceptions, it is impossible to overlook, because ignoring a !T is a compile error.
defer and errdefer
defer always runs at scope exit; errdefer runs only when the scope exits via an error. Together they eliminate the goto cleanup ladder that C requires and the partially-constructed-object leak that comes with it.
comptime
comptime executes ordinary Zig at compile time. It's the single mechanism behind generics, reflection, and code generation — there is no separate macro language and no separate template syntax.
Because it's the same language, comptime code is debuggable and readable — a genuine improvement over C preprocessor macros and C++ template metaprogramming.
Slices, optionals, and tagged unions
Build modes and safety
Safety checks catch integer overflow, out-of-bounds indexing, null unwrapping, and invalid casts — as a panic, at the point of failure. ReleaseSafe keeps them in production at modest cost, and is the right default for most services. Note that Zig does not prevent use-after-free or data races at compile time the way Rust does; the GeneralPurposeAllocator catches many of them at runtime in debug builds, but this is Zig's central safety difference.
C Interop and zig cc
Calling C directly
No binding generator, no FFI layer, no marshalling. Zig parses the C header at compile time and gives you the declarations. Exporting the other direction is equally direct:
This makes Zig unusually practical for incremental adoption: replace one .c file at a time in an existing codebase, or write a small native library that Python, Node, or Go calls over the C ABI.
Zig as a C/C++ toolchain
zig cc is Clang, bundled with libc headers for many targets, packaged as a single ~50 MB download. It cross-compiles without a sysroot, a container, or a toolchain per target:
For Zig code, cross-compilation is a flag on the ordinary build; there is no separate cross toolchain to install. Many projects — including ones with no Zig source at all — adopt zig cc purely as the simplest way to produce Linux, macOS, Windows, and WASM binaries from one machine. See WebAssembly.
The build system
build.zig is a Zig program, not a DSL:
Tests live inline with the code they test, which keeps them close and makes them easy to write:
Comparison
Zig vs. Rust is the comparison people actually want. Rust guarantees memory safety at compile time and has a mature ecosystem, a stable language, and Cargo; the cost is the borrow checker's learning curve and slow builds. Zig gives you a simpler language you can hold entirely in your head, dramatically better C interop, faster compiles, and explicit control — at the cost of no compile-time memory-safety guarantee and pre-1.0 churn. They aren't the same tool: Rust is the safer default for new systems software; Zig is compelling for C replacement and interop, embedded work, and codebases where explicitness matters more than static guarantees.
Where Zig fits well: replacing or extending C code, embedded and freestanding targets, WASM, CLI tools and databases where predictable performance and no GC matter, and as a cross-compiling toolchain for existing C/C++ projects.
Best Practices
Take the allocator as a parameter, always
Pair every acquisition with defer on the next line
Writing the cleanup immediately after the acquisition makes leaks a visual anomaly rather than a reasoning exercise. Use errdefer for anything ownership of which transfers on success.
Use std.testing.allocator in tests
It fails any test that leaks, which turns memory correctness into something CI enforces instead of something you audit.
Ship ReleaseSafe unless you've measured a reason not to
Safety checks catch overflow and out-of-bounds access with a clear panic rather than silent corruption. Move a specific hot function to ReleaseFast with @setRuntimeSafety(false) only after profiling proves it matters.
Prefer arenas for request- and phase-scoped work
Most server and compiler workloads have a natural lifetime boundary. An arena turns hundreds of frees into one and removes an entire category of leak.
Pin the Zig version
Pre-1.0 releases contain breaking changes. Commit the exact version to your toolchain config and CI, and read the release notes before upgrading — expect real migration work, not a version bump.
Handle errors at a boundary, not everywhere
try propagates cheaply. Let errors bubble to a layer that can actually decide something — the request handler, main, the task boundary — instead of catching at every level.
Common Mistakes
Assuming Zig prevents use-after-free
Returning a pointer into a dead stack frame
Ignoring an error union
Holding a slice across a resize
The same hazard applies to any slice into an ArrayList, a HashMap's values, or a buffer you might realloc. Zig has no borrow checker to stop you — treat "does anything here resize?" as a question you ask deliberately.
Treating undefined as zero
Building with the wrong optimization mode by accident
FAQ
Is Zig production-ready?
It's used in production — TigerBeetle (a financial database) and Bun (a JavaScript runtime) are the prominent examples — but it is pre-1.0 and the language still changes between releases. Adopt it if your team can absorb periodic migration work and values what it offers; choose Rust, Go, or C++ if you need a stable language contract today.
Zig or Rust?
Rust if compile-time memory safety, a mature crate ecosystem, and language stability are priorities — which is most new systems projects. Zig if you're interfacing heavily with C, need fast compiles and a language small enough to fully understand, are targeting embedded or freestanding environments, or want an easier cross-compilation story. Zig is not "Rust without the borrow checker" — it's a different bet, trading static guarantees for explicitness and simplicity.
Can I use Zig without writing Zig?
Yes, and many do. zig cc and zig c++ are a complete, cross-compiling Clang toolchain in a single download with bundled libc headers. Projects use it as their build compiler to produce Linux, macOS, Windows, and WASM artifacts from one machine — no Zig source involved.
Does Zig have async/await?
Async was removed from the language during a redesign and is being reworked; check the release notes for the version you're on rather than older tutorials. Current concurrent code uses OS threads (std.Thread), thread pools, or event loops built on std.posix primitives.
How is Zig's package ecosystem?
Small and young. build.zig.zon provides dependency management, and there's a growing set of libraries — but nothing approaching crates.io or npm. The mitigation is that Zig's C interop is so good that the entire C ecosystem is effectively available to you, which covers a lot of ground in systems programming.
Why do people say Zig is "simple"?
The language specification is genuinely small: no hidden control flow, no operator overloading, no macros, no destructors, no exceptions, no inheritance, and one metaprogramming mechanism (comptime) instead of several. You can read an unfamiliar Zig codebase and know that a line of code does what it says. Simple isn't easy — explicit allocation is more work than a GC — but the mental model is compact.
Related Topics
- C — the language Zig is designed to interoperate with and replace
- C++ — the other incumbent, and what Zig deliberately omits
- Rust — the main alternative for new systems work
- Systems Programming — the domain and its constraints
- Memory Management — allocators, arenas, and ownership
- WebAssembly — a first-class Zig target
- Build Tools — how
build.zigcompares to CMake and friends - Programming Languages — the broader landscape
- Benchmarking — measuring before choosing
ReleaseFast - Go — the GC'd alternative when you don't need manual memory control