Why Zig & Thinking in Zig
Origins and Purpose
Zig grew out of frustration with the maintenance burden of C++ and the hidden machinery of modern languages. The Zig team wanted a language for writing operating systems, compilers, embedded firmware, and high-performance servers where every behavior is predictable.
Three Design Goals
Three goals drive every design decision:
- Robust — catch bugs at compile time and at runtime in safety-checked builds, instead of letting undefined behavior silently corrupt your program.
- Optimal — no runtime overhead from language features you do not use; the generated machine code compares with C and C++.
- Reusable — a package and build system that makes cross-compilation and dependency management workable, plus seamless C interop that reuses the entire C ecosystem.
No Hidden Control Flow
In C++ or Rust, an operator like + can be overloaded to call an arbitrary function. In D, a property access obj.field can execute code. In Go and C++, a function may throw an exception that skips the rest of your function. Zig bans all of this: control flow happens only through visible keywords — if, switch, while, for, return, defer — and ordinary function calls.
Reading Code as Truth
Consider this Zig snippet. You can prove by reading it that foo() runs, then bar() runs, and nothing else can jump in between:
const result = compute(input); // only one function call — guaranteed
foo();
bar();
No Hidden Allocations
Most languages quietly allocate memory for you: strings grow, vectors resize, maps split buckets — all under the hood. Zig puts allocation in the call signature. If a function needs memory, it receives an allocator — a strategy object you choose — and its documentation says so.
The Allocator in the Signature
The consequence is profound: you always know which code touches the heap, and you can swap the allocator without touching business logic.
const std = @import("std");
// The function ASKS for memory: the allocator is a visible parameter.
fn greet(allocator: std.mem.Allocator, name: []const u8) ![]u8 {
// std.fmt.allocPrint allocates a fresh string from the provided allocator.
return std.fmt.allocPrint(allocator, "Hello, {s}!", .{name});
}
Reading the signature already tells you: callers decide where the memory comes from. If you call greet with an arena allocator, the string lives until the arena is destroyed — no manual free needed.
No Preprocessor, No Macros
Zig has no #define, no text-level preprocessor, and no macro system — because macros hide control flow and make code behave differently from how it reads. In their place, Zig offers comptime: a way to evaluate ordinary Zig code at compile time.
comptime versus Macros
You will study comptime in depth in the comptime lesson; for now, keep this mental model:
- C preprocessor — textual find-and-replace before the compiler sees the file.
- Zig comptime — real, typed code executed by the compiler while compiling.
Performance and Safety: Choose Two
Zig offers four build modes, and they are per-scope: a hot loop can disable safety while the rest of the program keeps it. This is the "choose two" trade-off made explicit and tunable.
The Four Build Modes
| Mode | Optimizations | Runtime Safety Checks | Typical use |
|---|---|---|---|
| Debug | Off | On | Development — best error messages, fastest compile |
| ReleaseSafe | On | On | Production binaries that fail loudly instead of corrupting silently |
| ReleaseFast | On | Off | Performance-critical paths where you have proven safety |
| ReleaseSmall | Size-focused | Off | Embedded targets and tiny binaries |
What Safety Checks Catch
Safety checks catch integer overflow, out-of-bounds indexing, misaligned access, and more — they turn bugs into snapshots (with stack traces) instead of silent memory corruption.
Zig versus C, C++, Rust
A short comparison to anchor expectations. Zig is not a superset of C like C++: it has its own clean syntax, while remaining able to import C code directly.
Feature Comparison
| Concern | C | Zig |
|---|---|---|
| Build system | Make/CMake — external tools | Built-in, declaration-based (build.zig) |
| Error handling | Return codes, no discipline | Error unions — failures are typed values |
| Memory safety | All undefined behavior | Checked by default; opt-out per scope |
| Nullability | Any pointer can be NULL | Optional types make nullability explicit |
| Metaprogramming | Preprocessor macros | comptime code execution |
| Allocations | malloc/free, implicit everywhere | Explicit allocator parameters |
Thinking in Zig — Summary
Adopt these five habits and the rest of the track will feel natural:
Five Habits
- Read code as truth. Whatever is visible is what runs — trust your eyes, not the docs.
- Ask "where is the allocator?" Before using anything that grows, find which allocator owns it and who frees it.
- Treat failure as a value. Errors are returned types, not exceptions that interrupt you mid-statement.
- Prefer compile time. If a value can be computed while compiling, make it
comptimeand the runtime cost becomes zero. - Choose safety by default. Ship with safety on; turn it off only where proven hot paths demand it.
Next, install the toolchain and run your first Zig program.