Why Zig Feels More Practical Than Rust for Real-World CLI Tools
Introduction
So when it comes to memory management there are two terms you really need to know, the stack and the heap.
The stack is a region of memory that stores temporary data that is only needed for a short period of time. It operates in a last-in, first-out (LIFO) manner, meaning that the most recently added data is the first to be removed, as the name suggests. Basically imagine a stack of plates, if you wanna remove one plate you remove the top one, remove the middle plate and disaster awaits in this analogy. The stack is typically used for storing function parameters, local variables, and return addresses. It is fast and efficient because it has a fixed size and does not require dynamic memory allocation.
The size of the stack is usually limited, and if a program tries to use more stack space than is available, it can result in a stack overflow error. This can happen if a function calls itself recursively too many times or if a program allocates too much memory on the stack.
Whereas the heap as the name suggests is a region of memory that is used for dynamic memory allocation. Unlike the stack, the heap does not have a fixed size and can grow or shrink as needed. The heap is typically used for storing data that needs to persist beyond the lifetime of a single function call, such as objects or data structures that are created at runtime. Imagine the heap as a pile of clothes in a disorganized household, you can add or remove clothes as needed and as long as the pile isn’t too big you can find what you need with relative speed and ease. But it will quickly become a nightmare if you let it grow out of control. The heap is managed by the operating system and requires dynamic memory allocation, which can be slower and less efficient than stack allocation.
The heap can also become fragmented over time, since we do not always store data in a contiguous block of memory. This can lead to performance issues and make it more difficult to allocate large blocks of memory.
Rust’s Borrow Checker
Rust’s borrow checker is a a pretty powerful tool that helps ensure memory safety during compile time. It enforces a set of rules that govern how references to data can be used, preventing common programming memory safety errors such as null pointer dereferencing, dangling pointers and so on. However you may have notice the word compile time in the previous sentence. Now if you got any experience at systems programming you will know that compile time and runtime are two very different things. Basically compile time is when your code is being translated into machine code that the computer can understand, while runtime is when the program is actually running and executing its instructions. The borrow checker operates during compile time, which means that it can only catch memory safety issues that can be determined statically, before the program is actually run.
This means that basically the borrow checker can only catch issues at comptime but it will not fix the underlying issue that is developers misunderstanding memory lifetimes or overcomplicated ownership. The compiler can only enforce the rules you’re trying to follow; it can’t teach you good patterns, and it won’t save you from bad design choices.
Story TIme
Last weekend I’ve made a simple CLI tool for myself to help me manage my notes it parses ~/.notes
into a list of notes, then builds a tag index mapping strings to references into that list. Straightforward, right? Not in Rust. The borrow checker blocks you the moment you try to add a new note while also holding references to the existing ones. Mutability and borrowing collide, lifetimes show up, and suddenly you’re restructuring your code around the compiler instead of the actual problem.
In Zig, we would just allocate the list with an allocator, store pointers into it for the tag index, and mutate freely when we need to add or remove notes. No lifetimes, no extra wrappers, no compiler gymnastics, that’s a lot more straightforward.
But Dave isn’t that the exact point of Rust’s borrow checker?
Yes it is, however by using Zig I managed to get most of the benefits of Rust’s memory safety without the complexity or ceremony of the borrow checker. All it took was some basic understanding of memory management and a bit of discipline. I was able to produce two CLI’s that are both memory safe and efficient however the Zig one was way more straightforward and easier to reason about and took less time to write.
What is Safety, Really for CLI Tools?
This is where a lot of developers trip up, Rust markets itself as a language that produces safe software, great marketing hook, but one tiny problem, memory safety is one puzzle piece of overall software safety. I’m not sure if the Rust foundation does this on purpose sort of a blanket statement to make it seem like memory safety is the end all be all of software safety, or if they just don’t want to constantly prefix safety with memory safety(even though they should).
But back to the main point, memory safety is just one aspect of software safety. You can argue if it’s a big or small piece of the puzzle, I’d say it depends on the software and use-case but it’s definitely not the only piece.
So What exactly is safety in terms of CLI tools?
Memory safety alone does not make a program safe. Your CLI tool can still crash, produce wrong results, corrupt files, leak sensitive data, be vulnerable to various types of attacks or just behave in a way that is not expected. Let’s go back to my Notes CLI
it’s rust version may never segfault but it could silently overwrite my index or tags or corrupt my files if I make a mistake in my logic, or perhaps it could store my file in a temporary location that is world readable, exposing my notes to anyone on the system. Is that safe? No.
Would using Zig solve any of those issues automatically, also no. Is my example a bit contrived, yes, but it illustrates the point that memory safety is not the only thing that matters when it comes to software safety.
In fact you should also consider other aspects of safety such as:
-
Predictable Behavior: The program should do what the user expects, even when input is malformed or unexpected. A CLI that panics on a missing file or fails silently on a corrupted note is not safe.
-
Avoiding Crashes or Silent Corruption: The program should handle errors gracefully, providing meaningful feedback to the user instead of crashing or corrupting data. A CLI that crashes on a malformed note or silently overwrites existing notes is not safe.
-
Manageable Performance: The program should perform well under expected workloads, avoiding excessive resource consumption or slowdowns. A CLI that becomes unresponsive when managing a large number of notes is not safe. This is where it really helps to understand memory allocations and performance characteristics of your language of choice.
-
Sensitive Data Handling: The program should protect sensitive data from unauthorized access or exposure. A CLI that stores notes in a world-readable temporary file is not safe.
-
Robustness Against Attacks: The program should be resilient against common attack vectors, such as injection attacks or buffer overflows. A CLI that can be exploited to execute arbitrary code or corrupt data is not safe. And this is precisely where Rust’s memory safety shines, it can help prevent certain types of vulnerabilities that arise from memory mismanagement. However, it’s not a silver bullet that guarantees overall safety.
The Borrow Checker: Strengths and Limitations
The borrow checker is impressive. It prevents dangling references, double frees, and mutable aliasing at compile time, things that would otherwise cause segfaults or undefined behavior. It’s why Rust can claim “memory safe without a garbage collector.”
Strengths:
-
Zero data races / mutable aliasing issues: The compiler guarantees that only one mutable reference exists at a time, and that immutable references cannot be combined with mutable ones.
-
Strong compile-time guarantees: Many memory-related bugs are caught before you even run the program.
-
Early bug detection: You find mistakes before shipping code, which is a huge win in long-lived services or concurrent systems.
Limitations / Pain Points:
Cognitive overhead: You’re constantly thinking about lifetimes, ownership, and borrow scopes, even for simple tasks. A small CLI like my notes tool suddenly feels like juggling hot potatoes.
Boilerplate and contortions: You end up introducing clones, wrappers (Rc, RefCell), or redesigning data structures just to satisfy the compiler. Your code starts serving the compiler, not the problem.
Compile-time only: The borrow checker cannot fix logic bugs, prevent silent corruption, or make your CLI behave predictably. It only ensures memory rules are followed.
- Edge cases get messy: Shared caches, global state, or mutable indexes often trigger lifetime errors that are annoying to work around.
At this point, the Rust borrow checker can feel more like a mental tax than a helpful tool, especially for short-lived CLI projects. You’re trading developer ergonomics for a compile-time guarantee that, in many CLI scenarios, may be overkill.
Zig’s Approach to Safety and Simplicity
Zig takes a different approach to safety and simplicity. It provides manual memory management with optional safety checks, allowing developers to choose the level of control they need. This can lead to more straightforward code for certain use cases, like CLI tools. However where it really shines is how it does manual memory management, I’ve briefly touched upon this in my other blog post Zig Allocators Explained.
But basically long story short Zig gives you allocators, a set of tools that helps you manually manage your memory in a more structured and predictable way. You can choose to use a general purpose allocator like the std.heap.page_allocator
or you can create your own custom allocator that fits your specific needs. This allows you to have more control over how memory is allocated and deallocated, which can lead to more efficient and predictable memory usage. This combined with Zig’s defer
statement which allows you to schedule cleanup code to run when a scope is exited, makes it easy to manage resources gives you most of the power of Rust’s borrow checker at your disposal without the complexity and ritual. However it asks one thing in return of you, discipline, your software will be only as safe as you make it. We can make the same claim about Rust, you can throw copy
and clone
and unsafe
around your code and throw away all the benefits of the borrow checker in a heartbeat.
The two languages are polar opposites in this regard, Zig places the burden on the developer and makes it easy for them to produce memory safe software, whereas Rust places the burden on the compiler and makes it hard for developers to produce memory unsafe software.
Back to the main point, zig’s approach to memory management is in my subjective opinion more practical for most of my use cases, especially for CLI tools. It allows me to write straightforward code that is easy to reason about and maintain, without the overhead of the borrow checker. I can allocate a list of notes, store pointers to them in a tag index, and mutate the list freely when I need to add or remove notes. No lifetimes, no extra wrappers, no compiler gymnastics, that’s a lot more straightforward.
Oh I almost forgot, Zig also has the comptime
feature which allows you to execute code at compile time. This can be useful for generating code, performing static analysis, or optimizing performance and even for testing which is a really nice bonus and can be a small helper when it comes to memory safety.
Developer Ergonomics Matter and Developers are not Idiots
When developing software we want to be productive and efficient, most of all we want to be correct and produce good software, however we also want to enjoy the process of creation and not feel like we are fighting the tools we use. Developer ergonomics is a term that refers to how easy and comfortable it is to use a programming language or framework. It encompasses things like syntax, tooling, documentation, and community support. A language with good developer ergonomics can make it easier to write correct code, while a language with poor developer ergonomics can make it harder to do so. I’d say as it currently stands Rust has poor developer ergonomics but produces memory safe software, whereas Zig has good developer ergonomics and allows me to produce memory safe software with a bit of discipline.
I personally usually prefer languages where I do not have to succumb to too much ceremony and ritual to get things done, I want to be able to express my ideas in code without having to constantly think about the underlying mechanics of the language and yet I want to be responsible and produce good software. So with C and C++ this was a tiny bit harder as you basically had to learn some useful and practical memory management patterns and techniques, Zig comes with them baked in.
I feel like Zig really respects it’s developers and treats them like adults, it gives you the tools and expects you to use them wisely. Rust on the other hand feels like it treats developers like children that need to be constantly supervised and guided, which can be frustrating and demotivating.
Developers are not idiots, sure even the smartest amongst us still produce memory safety issues or bugs in their software and it’s silly to assume that with enough training and practice we can become perfect developers, but we can become better developers. We can learn from our mistakes and improve our skills, we can learn to write better code and produce better software.
It’s not good to abstract that away to the compiler and assume that it will magically make us better developers, I don’t personally think it will. In fact not to sound too cliche but I think that the journey to becoming a better developer is a series of mistakes and fixes, we learn from our mistakes and improve our skills. What does it say about a language that tries to abstract away the mistakes we make, does it really help us become better developers ?
Final Thoughts
Rust is amazing, if you’re building something massive, multithreaded, or long-lived, where compile-time guarantees actually save your life. The borrow checker, lifetimes, and ownership rules are a boon in large systems.
But for small, practical CLI tools? Rust can feel like overkill. That’s where Zig shines. Lightweight, fast, and straightforward, you get memory safety without constantly bending over backward for the compiler. You can allocate a list, track pointers, and mutate freely without extra wrappers, lifetimes, or contortions. Iterating feels natural, the code is easier to reason about, and you get stuff done faster.
Memory safety is important, but it’s just one piece of the puzzle. Predictable behavior, maintainable code, and robustness are just as critical, and that’s exactly why Zig often feels more practical for real-world CLI tools.
At the end of the day, it’s not about which language is “better.” It’s about what fits your workflow and the kinds of projects you build. For me, Zig hits the sweet spot: memory safe, low ceremony, and developer-friendly, perfect for small tools that actually get things done.
References
- The Stack and the Heap
- Zig Allocators Explained
- Rustonomicon - The Dark Arts of Unsafe Rust
- Zig Documentation - Memory Management
- Rust Documentation - The Rust Programming Language
- Zig Documentation - Comptime
- Rust Documentation - Ownership
- Zig Documentation - Defer
- Rust Documentation - Error Handling
- Zig Documentation - Error Handling
- Rust Documentation - Concurrency
- Zig Documentation - Concurrency
- Rust Documentation - Testing
- Zig Documentation - Testing
- Rust Documentation - Performance