The Zig programming language continues its gradual advance into the world of systems programming. Even though it has yet to hit version 1.0, Zig is already in use with several production projects and attracts admirers with its slew of good ideas and commitment to quality.
Started by Andrew Kelley in 2015, Zig is a very active project and now seems to be reaching critical mass. Its rather momentous ambition is to become the heir to the C programming language, which has reigned for decades as the go-to portable low-level language.
This article is a high-level introduction to Zig, including its design goals, memory and error handling, conditional compilation, interoperability with C and C++, and a feature update for the latest version as of this writing.
Could Zig replace C?
Zig is described as a “low-level systems language,” but what is low-level? Systems language is also fairly ambiguous terminology. I asked Loris Cro, VP of community at the Zig Software Foundation, how he describes Zig. He said, “I define Zig as a general-purpose programming language because, while it’s an obvious good fit for systems programming, it’s also suited for programming embedded devices, targeting WebAssembly, writing games, and even most tasks that would normally be handled by higher-level languages.”
Zig may be easiest to understand as it relates to C; that is, as a general-purpose, non-garbage-collected, and portable language with pointers. Today, virtually all programming infrastructure rests on C in various ways, including being the foundation of other programming languages like Java, JavaScript, and Python. If Zig were to be adopted broadly as an archetypal replacement for C, it could have enormous benefits. Just imagine the ripple effect of an industry-wide evolution to a language that is like C, but safer, faster, less buggy, and easier to maintain.
Zig’s design goals and syntax
Zig is a “close to the metal” language in that it allows developers to work directly with system memory, a requirement for writing code that can be maximally optimized to its task. Direct memory allocation is a characteristic shared by the C family, Rust, and other low-level systems languages. Zig offers similar capabilities but aims to improve on them in several ways.
Zig seeks to be a simpler systems-oriented language than its predecessors and make it easier to write safe, correct code. It also aims for a better developer experience by smoothing the sharp edges found in writing C-like software. On the first review, Zig’s features might not come across as earth-shattering. But in overall effect, it is a safer platform that developers are finding easier to master and use.
Currently, Zig is being used to implement the Bun.js JavaScript runtime as an alternative to Node.js. Bun’s creator Jarred Sumner told me “Zig is sort of similar to writing C, but with better memory-safety features in debug mode and modern features like defer
(similar to Go’s) and arbitrary code can be executed at compile time via comptime
. It has very few keywords so it’s a lot easier to learn than C++ or Rust.”
Zig is also used as the language for the TigerBeetle database. TigerBeetle’s rationale for choosing Zig describes Zig as “a DSL for machine code” whose compile-time features make it “very easy to directly express what you want the computer to do.” In essence, Zig lets you describe what the machine is doing very explicitly, with a minimum of extra syntax, without losing expressiveness.
Zig lacks an interface (or something similar, like Rust’s traits) to define contracts on types, which TigerBeetle acknowledges “comes at the cost of missing declaration-site interfaces.” According to TigerBeetle’s developers, this feature is “less important in a zero-dependency context.” Instead, Zig lets you do a similar kind of generic programming, checked by the compiler at compile time.
Other production projects using Zig include the Ghostyy terminal emulator and Uber, which uses Zig for builds. There are also examples of using Zig to speed up Python.
Zig differs from most other languages in its small feature footprint, which is the outcome of an explicit design goal: Only one obvious way to do things. Zig’s developers take this goal so much to heart that for a time, Zig had no for loop. That design decision has since changed, and Zig now has a fully functional for
loop.
Kevin Lynagh, coming from a Rust background, wrote, “The language is so small and consistent that after a few hours of study, I was able to load enough of it into my head to just do my work.” Nathan Craddock, a C developer, echoed the sentiment. Programmers seem to like the focused quality of Zig’s syntax.
How Zig handles memory
A distinctive feature of Zig is that it does not deal with memory allocation directly in the language. There is no malloc
keyword like we have in C and C++. Instead, access to the heap is handled explicitly in the standard library. When you need such a feature, you pass in an Allocator
object. This has the effect of clearly denoting when memory is being engaged by libraries while abstracting how it should be addressed. Instead, your client code determines what kind of allocator is appropriate.
Making memory access an obvious library characteristic is meant to avoid hidden allocations, which is a boon to resource-limited and real-time environments. Memory is lifted out of the language syntax, where it can appear anywhere, and its handling is made more explicit.
Allowing client code to specify what type of allocator it passes into an API means the code gets to choose based on the environment it is targeting. That means library code becomes more obvious and reusable. An application can determine when exactly a library it is using will access memory, and hand it the type of allocator—embedded, server, WebAssembly, etc.—that is most appropriate for the runtime.
As an example, the Zig standard library ships with a basic allocator called a page allocator, which requests memory from the operating system by issuing: const allocator = std.heap.page_allocator;. See the Zig documentation for more about available allocators.
Zig also includes safety features for avoiding buffer overflows, and it ships with a debug allocator that detects memory leaks.
Conditional compilation in Zig
Zig uses conditional compilation, which eliminates the need for a preprocessor as found in C. Therefore, Zig does not have macros like C and C++. From a design standpoint, Zig’s development team views the need for a preprocessor as indicative of a language limitation that was crudely patched over.
Instead of macros, Zig’s compiler determines what code can be evaluated at compilation time. For example, an if
statement will eliminate its dead branch at compile time if possible. Instead of using #define
to create a compile-time constant, Zig will determine if the const
value can be treated that way and just do it. This not only makes code easier to read, write, and think about but also opens up the opportunity for optimization.
As Erik Engheim writes, Zig makes compile-time computing a central feature instead of an afterthought. This allows Zig developers “to write generic code and do meta-programming without having any explicit support for generics or templates.”
A distinctive Zig feature is the comptime keyword. This allows for executing code at compile time, which lets developers enforce types against generics, among other things.
Interoperability with C and C++
Zig supports a high degree of interoperability with C and C++. As stated in the Zig docs: “Any language that does not have the ability to interact with C code risks obscurity.”
Zig can compile C and C++. It also ships with libc
libraries for many platforms. It can build these without linking to external libc
libraries. See this Reddit thread for a detailed discussion of Zig’s relationship to libc
.
For an in-depth discussion of Zig’s C compiler capability, see Andrew Kelley’s blog post on the topic, which includes a demo of Zig compiling the GCC LuaJIT compiler. The bottom line is that Zig attempts not only to replace C with its own syntax, but actually absorb C into itself as much as possible.
Loris told me that “Zig is a better C/C++ compiler than other C/C++ compilers since it supports cross-compilation out of the box, among other things. Zig can also trivially interoperate with C (you can import C header files directly) and it is overall better than C at using C libraries, thanks to a stronger type system and language features like defer.”
Error handling in Zig
Zig has a unique error-handling system. As part of its “avoid hidden control flow” design philosophy, Zig doesn’t use throw
to raise exceptions. The throw
function can branch execution in ways that are hard to follow. Instead, if necessary, statements and functions can return an error type, as part of a union type with whatever is returned on the happy path. Code can use the error object to respond accordingly or use the try
keyword to pass up the error.
An error union type has the syntax !
. You can see this in action with the simple “Hello, world” example (from the Zig docs):
const std = @import("std");
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
try stdout.print("Hello, {s}!n", .{"world"});
}
Most of this code is self-explanatory, but the !void
syntax is interesting. It says the function can return either void or an error. This means if the main()
function runs without error, it will return nothing; but if it does error out, it will return an error object describing the error condition.
You can see how client code can use an error object in the line where the try
keyword appears. Since stdout.print
can return an error, the try
expression here means the error will be passed up to the main()
function’s return value.
Zig toolchain and testing
Zig also includes a build tool. As an example, we could build and run the above program with the commands shown here (this is again from the Zig docs):
$ zig build-exe hello.zig
$ ./hello
Hello, world!
Zig’s build tool works in a cross-platform way and replaces tools like make
and cmake
.
As of 2025, Zig’s build system includes an integrated package manager with a standard configuration file, build.zig.zon
. It uses Zig Object Notation (ZON), the .zon
file format, which you can learn more about here. ZON is like Zig’s take on JSON.
The standard command line is often sufficient for Zig programs, but the package manager lets you address situations where you might need to provide a package to be consumed by third parties, build many things, or where the build process contains many steps.
Evolving beyond LLVM
Zig originally was built on top of the LLVM toolset but has since moved to make LLVM an optional component. This helps to make the Zig language portable and more amenable to freestanding builds. This update is interesting because it implies the Zig toolchain is moving toward using Zig itself for much of the back-end infrastructure, what Zig’s developers are calling self-hosted Zig.
This means that where once Zig was largely a front-end language syntax to the LLVM, it is now an entire stack of tools that can take that front end and use different options for outputting the implementation to target systems. Most of those target operating systems have Zig back ends that are not dependent on LLVM, although you can still use LLVM and in some cases, C compatibility may still require it.
The tendency today is to use self-hosted for development, for the faster build times, then move to the LLVM for the most optimized runtime production build.
State of Zig
Zig has an active Discord community and a lively GitHub ecosystem. The in-house documentation is pretty thorough. Zig users have produced a good amount of third-party material, as well.
Zig is still a relatively young project, in version 0.14.0 at the time of writing. The slow pace of development reflects the team’s devotion to quality. Several real-world projects are already using Zig in production. On the topic of production readiness, Loris said, “Zig is not yet at v1.0, so things like webdev are still in their infancy, but the only usage that I would consider not recommending Zig for is data wrangling, for which I think a dynamic language like Python or Julia would be more practical.”
It’s worth noting that web development still might not be the first thing you think of for Zig, but its Zine static site generator has matured considerably and is now hosting the Zig website. Zig also supports several web-related projects.
You can watch the progress towards the 1.0 release in this GitHub issue. There is no target release date, but many of the issues appear to be more on the order of niceties than critical fixes.
For now, the Zig team appears to be taking its time with the 1.0 release, which may drop in 2025 or later—but none of that stops us from building all sorts of things with the language today. Zig’s activity, goals, and uptake by the developer community make it a compelling project to watch.