You think you’ve been writing safe code… until you meet the Rust borrow checker.
Then suddenly, your once-proud instincts are being side-eyed by a compiler that’s not mad, just disappointed.
Today, on Day 11 of my Rust journey, we talk about the infamous, unyielding, sometimes infuriating, but ultimately brilliant guardian of Rust safety: the borrow checker.
First Contact: Compiler Says No
It started innocently enough. I wrote this:
fn main() { let mut message = String::from("Hello"); let r1 = &message; let r2 = &mut message; // ERROR! println!("{}, {}", r1, r2); }
And BAM… compile-time failure.
What? I just wanted to read and write the same variable! C# would’ve shrugged and let me do it. Rust?
“You’re trying to borrow
message
as mutable while it’s still borrowed as immutable. I’m not mad. I’m just preventing undefined behavior.”
Why So Strict?
Because the borrow checker is doing what the garbage collector can’t: enforcing safe memory access rules at compile time.
In C#, I could be reading from a variable while something else is writing to it. In multithreaded code, this is a landmine. That’s why we have lock
, volatile
, and a few prayers.
Rust’s answer: don’t allow that situation ever.
You get either:
- One
&mut
mutable reference - Or any number of
&
immutable references
But never both at the same time.
A Real-World Example
Let’s say I wanted to update a field in a struct while also printing it:
struct Profile { name: String, } fn main() { let mut profile = Profile { name: String::from("Alice") }; let name_ref = &profile.name; profile.name.push_str(" Smith"); // ERROR println!("Name: {}", name_ref); }
The compiler’s response? Nope. I’m still using name_ref
when I try to mutate profile.name
. Although this may seem safe in C#, Rust considers it a violation of the borrowing contract.
The Lesson: Think in Lifetimes
What the borrow checker really teaches you is how long your variables live and who owns them.
It makes lifetimes feel real, not abstract.
The moment I started thinking, “Who owns this?” and “How long will this reference stick around?”, my code got clearer, not just in Rust, but even in my C# brain.
Getting Around It: Scope Ends = Borrow Ends
Sometimes, it’s just a matter of scope:
fn main() { let mut name = String::from("Alice"); { let r1 = &name; println!("{}", r1); } // r1 goes out of scope here let r2 = &mut name; // OK now! r2.push_str(" Smith"); println!("{}", r2); }
This compiles because r1
’s lifetime ends before r2
starts. The borrow checker isn’t trying to ruin your day, it’s just making sure you actually stop using something before you mutate it.
Why It Feels So Different from C#
In C#, you don’t really think about lifetimes. The GC handles cleanup, and the runtime handles access. But that flexibility comes at a cost: runtime bugs, potential data races, and memory churn.
Rust flips the script. It gives you compile-time certainty that you aren’t doing anything sketchy. But that certainty comes with a learning curve.
Final Thoughts: From Frustration to Respect
At first, I’ll be honest, the borrow checker felt like a wall.
But now? I see it as a mentor. One that says:
- “No, you can’t do that yet.”
- “You need to think more carefully.”
- “I’m here to help you write code that can’t crash because of memory issues.”
That’s not annoying. That’s empowering.
Tomorrow, we’ll ease into something a little more familiar again: strings and slices… aka goodbye StringBuilder
, hello UTF-8.