My Rust Journey: My Two Cents

From Fighting the Borrow Checker to Thinking in Rust

JustATheory published on
14 min, 2792 words

I recently saw a post on Rust saying the following

The best advice for learning Rust is ... not to learn it!

And obviously, I laughed and felt a bit proud of being a professional Rust programmer. After a few seconds, when my pride-filled, inflated chest dropped back down, and I returned to reality, I asked myself: Why do people find Rust so difficult?

Then I remembered why I started learning Rust. It was because there was a lot of buzz around how difficult the language is—how you can’t just conquer it by learning the syntax alone, and how you supposedly need a brain like Megamind to learn it and be productive with it. Now, I have this habit of picking up things simply because they are difficult. Part of the reason is that these things are often a lot of fun to learn. Another reason is the validation you get—not just externally, but internally—that your brain is still working fine and that you can still learn new things, starting as a beginner and eventually becoming comfortable with the subject. I had a lot of time after my job back then, so I thought it would be a nice activity to learn a language that people kept saying was so difficult.

I started learning from the amazing official Rust Programming Book. Initially, it was quite a smooth journey, covering basic stuff like syntax around variables, data types, functions, etc. All of this is usual in any programming language, so it didn’t take much time to absorb. Then came the chapter on Understanding Ownership—the topic everyone talked about! The topic is so mysterious that there might as well be a cult around it! I heard this term in almost every post discussing why Rust is hard and counterintuitive, and it always came down to Rust’s ownership model.

Anyway, I started reading the chapter with a bit of horror and uncertainty about whether I would be able to understand any of it. There was a great explanation of the stack and heap, how collections work, how their bulk data lives on the heap, and so on. This knowledge wasn’t entirely new to me, because if you’ve been a CS student, you already know these terms and the surrounding concepts. I thought, That was easy! Something felt off. People can’t be ranting so much over these simple concepts of stack vs heap, who owns the memory, who frees it, etc. And that is supposed to be the hardest thing about Rust? The ownership model itself isn’t difficult to understand while learning it. Even move semantics—though new to many programmers—aren’t that hard to grasp. The real hurdle comes when you start writing programs and Rust doesn’t let you do things that you can easily do in other languages.

This phenomenon is famously called "fighting the borrow-checker". And these issues aren’t something you encounter only after writing basic or intermediate Rust code comfortably. The borrow-checker starts bugging you (pun intended) right at the very beginning of your journey.

For example, consider a simple Rust program:

let s1 = String::from("Rust is Love");
let mut s2 = String::from("");

s2 = s1;

println!("s1 is: {:?}", s1);

This code won’t compile! This is about as basic a program involving strings can be, and programmers want to do this kind of thing all the time. Well, you can’t in Rust. Now, I won’t turn this post into a Rust teaching session—that’s something I plan to do in subsequent posts. But yes, the borrow-checker shows up right at the very start, and you might get frustrated, asking: “Why is it the way it is?!” If you find yourself asking this question, read the ownership chapter again. The problem isn’t that the ownership chapter is difficult. Almost every serious programmer who reads it agrees with its principles. The issue is that, at that point, they don’t yet see the implications of how counterintuitive it can make writing programs in Rust. This is the steep learning curve people talk about: going from writing code the way you would in other languages and fighting the borrow-checker, all the way to writing what’s called Rustonomic (Rust-ergonomic) code. This journey is, in fact, long compared to other languages, where you can gain productivity in days—or even hours, if you’re already experienced (and used to working with product managers).

It took me around a year to become comfortable writing Rustonomic code—and that too while I was developing my own compiler for a Python-like, statically typed language called Jarvil. Honestly, the reason I started learning compilers was that I wanted to build software difficult enough to truly use—and thereby learn—Rust in its full glory. You can’t really learn Rust by writing the same routine web services or CLIs that you build while learning other languages. In many languages, the goal is simply to become comfortable with the syntax. In Rust, that’s not enough—you won’t even get to use many core Rust concepts like smart pointers, lifetimes, traits, generics, and so on. And these are precisely the things that make Rust so performant. So if you’re not using Rust professionally, you might be in for a much longer journey before you truly become a Rustacean—which I don’t think is a bad thing at all. After all, Rust is a systems language. That means it can appear verbose, because it targets people building software where they want access to these low-level, explicit nuts and bolts, so they can compose them in ways that aren’t possible in much higher-level (but less verbose) languages like Python. Such software includes compilers, databases, operating systems, browsers, text editors, terminal emulators, firmware, kernel drivers, virtual machines, game engines, or even a very intentional hello world.

Anyway, I eventually grew out of the project after working on it for around two years—learning Rust, unlearning many bad ways of writing Rust (some of which I discuss below), and slowly learning the Rust way of doing things. I didn’t realize it at the time, but learning Rust didn’t just make me a better Rust programmer; it made me a better programmer overall. Writing a full-blown compiler definitely helped in that journey. Eventually, I gained real productivity in Rust. I was no longer fighting the borrow checker. My brain could anticipate—before writing any code—what the Rust way of doing something would be, so that the borrow checker would be happy. Even today, there are a few light skirmishes here and there, but those are mostly due to laziness or sloppiness on my part. They’re never frustrating anymore, which is usually the case when you’re a beginner. After that, I went on to implement another compiler—this time as a professional Rust programmer for my organization. I also built a vector database, an async runtime for WASM, an agentic framework, GPU host-side safe Rust bindings and several other crates. And honestly, implementing these softwares and becoming a systems programmer feels so rewarding.

I probably wouldn’t have become a systems programmer if Rust didn’t exist.

Now, for people who are trying to learn Rust, there are common ways beginners try to work around the borrow checker just to make their code run, often at the cost of performance and by relying on constructs that are generally discouraged in a truly Rustonomic sense.

Beginners Pitfalls

Cloning everything:

The above program can be made to work like this:

let s1 = String::from("Rust is Love");
let mut s2 = String::from("");

s2 = s1.clone();

println!("s1 is: {:?}", s1);

This code compiles, and there’s no more fighting the borrow-checker. But notice the .clone(). A lot of programs can be made to work by cloning everywhere, but the price you pay is heavy. Calling clone on collection types like String results in a deep copy, which means an O(len) copy of data on the heap. Most of the time, this is not what a programmer wants. One might ask how other languages handle this.

For example, in Python:

s1 = "Rust is Love"
s2 = s1

When s2 = s1 is executed, internally only the pointer to the string stored on the heap is copied to s2. This is called shallow copying or copying by reference, and hence the operation is cheap. Now one might ask: “Why doesn’t Rust do the same?” Because of ownership, an answer to almost every question in Rust involves ownership and the borrow-checker. Rust’s ownership model states that a location in memory is owned by a single variable, and that variable is responsible for freeing the memory when it goes out of scope. There can’t be two owners of the same memory, because when both go out of scope, they would both try to free it, resulting in the classic double-free memory bug.

In Python, the heap memory is effectively owned by both variables s1 and s2, but memory is managed by a reference-counting garbage collector. The memory is freed only when there are no active references left—that is, when both s1 and s2 go out of scope and the reference count drops from 2 to 0. One might then ask: “Why doesn’t Rust just use a GC?” Because garbage collection introduces runtime overhead, which adds to the execution cost of a program, something many systems programmers are not willing to trade for the convenience that GC provides. This can even give you insight into why Rust is so verbose. This verbosity is necessary because the usual "convenient" ways of doing things potentially have runtime overhead. That's why you often find Rust evangelists talk about zero-cost abstractions in Rust. This generally means that most abstractions in Rust (even things like generics and traits) have no extra cost at runtime!

Now saying each piece of memory is owned by a single variable in Rust is not entirely true. We can mimic what Python do in Rust as well, using a special kind of smart pointers called Rc<T> (for single-threaded use-cases) or Arc<T> (for multithreaded use-cases). So you can wrap any type inside an Arc, and then cloning would just copy the pointer and increase the reference count to the memory location. This brings me to the second pitfall of a beginner.

Using Rc or Arc for every variable in order to keep cloning cheap.

Now, I’m not saying you should never use Rc or Arc in your code. They are important for certain use cases and become even more important in multithreaded environments. However, beginners often overuse them, even in situations where simple references would work just fine. Generally, Rc or Arc is used when ownership of a memory location cannot be clearly determined at compile time. This commonly happens in multithreaded environments, because the compiler can’t know the order in which threads will execute at runtime. As a result, shared data across threads is wrapped inside an Arc. This is a perfectly valid and understandable use case, and in many such situations, there’s no real alternative to using Arc (Even here, a lot can be achieved with message passing and channels—but we’ll talk about that some other day). Now one might ask, “How do I learn when to use what?” And the answer is simple: you learn it by writing Rust programs—lots of them, and increasingly complex ones. No one can teach you that intuition. That’s what makes Rust hard: it’s not just about learning the syntax, but about developing that intuition.

I remember using clones and Rc everywhere in my Jarvil codebase. As I matured, I would read my old code and genuinely feel sick—it was just ugly. In fact, Rc looks uglier to me now than Arc! I went through the entire codebase, found every usage of Rc, and replaced it with simple references and lifetime annotations wherever possible. It was slow. It was painful. And yes, painful again. But it was necessary—and a major step in maturing in my Rust learning journey.

So my advice for learning Rust is simple: just start.

Follow the book. Write simple programs. Fight the borrow checker. Use clones to get things working. Write more complex programs. Read your old code. Feel sick. Recover. Replace clones with references. Feel proud. And then fight the borrow checker all over again.

Here are a couple of resources I strongly recommend for practicing Rust:

I also recommend watching RustConf talks even if you don't understand them completely! I also did not initially, but it kept my excitement and motivation to learn Rust ever-growing, even today.

Now, with reference-counted pointers, one issue is that you can only obtain immutable references to the underlying memory. “Why?” you may ask—and as I mentioned earlier, the answer to almost every question in Rust comes back to ownership. Rust enforces the rule that you can have either multiple immutable references or exactly one mutable reference to a memory location at a time. This rule exists to eliminate data races and dangling-pointer bugs at compile time. To work around this restriction when mutation is required, Rust provides another concept called interior mutability, implemented through types like RefCell<T>. This brings me to the third—and last—issue I often see in beginner (and even intermediate) Rust code.

Using Rc<RefCell<T>> for all your data-structures

In short, RefCell allows you to obtain a mutable reference from an immutable one, which means you can mutate data that is owned by multiple variables (usually through cloning a Rc). However, unlike normal borrowing, these checks happen at runtime rather than at compile time. This means your program can panic if you violate the borrowing rules—for example, by attempting multiple mutable borrows. RefCell is ugly to me now as well! And in practice, you almost never need it—at least in single-threaded contexts. In multithreaded scenarios, you would typically use a Mutex, which enforces mutual exclusion through locking. A Mutex may block until the lock is acquired, but it won’t panic due to borrow rule violations. I’ve written sufficiently complex Rust code, and I can confidently say that in most cases, you can avoid RefCell entirely. So avoid it at all costs (pun intended—because it does have a runtime cost).

At this point, an experienced beginner (yes, that’s a real thing in Rust) or an intermediate Rust programmer might ask: “Then how are we supposed to write complex data structures like trees or graphs, where we clearly need multiple ownership and mutation?”

My answer: Arenas.

Arenas deserve a post of their own—just as long as this one, if not longer—and I’ll definitely write about them given how important they are for writing truly Rustonomic code.

For now, I’ll share an excellent talk by Katherine West on the topic: https://youtu.be/aKLntZcp27M

Arenas were the last major concept I learned before becoming a true Rustacean—before my code started looking like the crates I used to read while learning how to write idiomatic Rust. Once you have some experience writing Rust, you should start reading other people’s crates.

Pick any domain you enjoy:

When your code starts looking like familiar crates, that’s when you truly become a Rustacean.

Conclusion

An amazing property of Rust is that it enforces correct design patterns by pushing them onto the programmer through the borrow checker. In many other languages, programmers have to learn these patterns explicitly—and some may spend their entire careers without ever truly learning them. You can see this clearly after watching Katherine West’s talk: how generational arenas provide solutions to many borrow-checking problems that intermediate Rust programmers often bypass using Rc<RefCell<T>>, but which is actually the correct design pattern. These are the same patterns that the C++—especially the game development—community learned the hard way, eventually adopting data-oriented designs like the Entity–Component System (ECS).

And that's why I said earlier that learning Rust not only makes you a better Rust programmer but an overall better programmer and computer science enthusiast. So if you really want to learn how a computer works, learn Rust!

I’ll definitely try to regularly share more about the topics I touched on in this post—and even more—in subsequent posts. I also promise to make them much more technical, with real code, diagrams, and concrete examples, and less vague or hand-wavy.