Zig Allocators, Error Unions, and C Interoperability
Zig makes two things explicit that most languages hide: memory allocation and error handling. There is no global allocator — any function that allocates takes an Allocator parameter, so callers decide where memory comes from and readers can see which functions allocate. And errors are values: a function returns an error union such as ParseError![]i64, and callers must handle it with try, catch, or an explicit switch.
These choices make Zig well suited to writing libraries that other languages call through the C ABI, one of Zig's main use cases alongside replacing or extending C codebases. This page builds a small library with explicit allocation and error paths, tests it for leaks, and exposes it to C with clear ownership and error conventions.
TL;DR
- Functions that allocate take a
std.mem.Allocator; document who owns returned memory and how to free it. - Use
deferfor unconditional cleanup anderrdeferfor cleanup that runs only if the function returns an error. std.testing.allocatorfails tests that leak memory; arena allocators free everything at once for request- or task-scoped work.- Error sets list the errors a function can return; error unions (
E!T) are handled withtry,catch, and exhaustiveswitch. export fnexposes functions with the C ABI; map Zig errors to status codes and use C-compatible types at the boundary.- Test the boundary from C as well as from Zig, and pin the Zig version, since the language is still pre-1.0.
Quick Example
A parser that returns caller-owned memory, with leak-checked tests (parse.zig):
Exposing it to C with status codes and checked arithmetic:
Run the tests with zig test parse.zig; build a shared library that links libc (for c_allocator) with your project's build.zig or zig build-lib.
Core Concepts
Allocators and Ownership
Names and defaults in std.heap evolve between Zig releases; check the documentation for your version. Whatever the allocator, state the ownership rule in each function's doc comment: who frees, and with which allocator. See Memory Management.
defer and errdefer
defer runs when the scope exits; errdefer runs only when the function returns an error from that point on. Together they express "free this if I fail, hand it to the caller if I succeed" without duplicated cleanup code — the core pattern for functions that build and return allocated data.
Error Sets and Error Unions
- An error set (
error{ Empty, InvalidNumber }) names possible failures; sets merge with||. - An error union
E!Tholds either an error fromEor a value ofT;!Tlets the compiler infer the set. try xreturns the error to the caller;x catch |err| ...handles it;switch (err)on a known set must be exhaustive, so adding a new error forces call sites to handle it.- Errors carry no payload; return extra detail through an out-parameter or diagnostics struct when needed. See Error Handling.
Safety and Checked Arithmetic
Debug and ReleaseSafe builds trap integer overflow, out-of-bounds access, and unwrapping null. For values derived from untrusted input, use std.math.add, std.math.mul, and friends, which return errors instead of trapping, and validate lengths before slicing.
The C Boundary
Zig also works as a C/C++ compiler and cross-compilation toolchain (zig cc), which is often how teams adopt it gradually. See C and Rust Unsafe Code and FFI for a comparable boundary in Rust.
Best Practices
Pass Allocators Explicitly
Accept an allocator parameter in library functions rather than choosing one internally, except at C boundaries where the convention must be fixed.
Pair Every Allocation With defer or errdefer
Write the cleanup line immediately after the allocation so it is never forgotten.
Test With the Leak-Checking Allocator
Run all tests with std.testing.allocator, including error paths, so partial-failure leaks are caught.
Design the C API Around Ownership and Status Codes
Document every exported function's ownership, error codes, and thread-safety, and test it from C.
Common Mistakes
Freeing With the Wrong Allocator
Missing errdefer
Letting Errors or Panics Escape Into C
FAQ
Why does Zig pass allocators explicitly?
So callers control where memory comes from and every allocation is visible in function signatures, which makes libraries flexible and memory behavior easy to audit and test.
What is the difference between defer and errdefer?
defer always runs when the scope exits. errdefer runs only if the function returns an error after that point, which is ideal for freeing resources that would otherwise be returned to the caller.
How does Zig detect memory leaks in tests?
std.testing.allocator tracks allocations during each test and fails the test if anything allocated was not freed.
How do I call Zig from C?
Mark functions with export, use C-compatible parameter and return types, build a static or shared library, and provide a C header declaring the functions.
Is Zig stable enough for production?
Zig is pre-1.0 and its language and standard library still change between releases. Teams use it in production by pinning the compiler version and budgeting for upgrades.
Related Topics
- Zig — The Zig language
- C — The ABI and language Zig interoperates with
- Memory Management — Allocation strategies and ownership
- Systems Programming — Low-level software design
- Rust Unsafe Code and FFI — Foreign function interfaces in Rust
- Error Handling Patterns — Error handling across languages