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

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

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

References