There's a stage in learning Rust where the borrow checker feels like an adversary. You write something that would be perfectly fine in any other language, the compiler refuses, and you spend twenty minutes adding and removing ampersands like you're defusing a bomb by trial and error. I lived in that stage for a few weeks. This is the writeup of how I got out of it, which mostly amounts to realising the checker was right almost every single time and I was the one being unreasonable.
The shift I want to describe isn't a list of tricks. It's a change in posture. I stopped reading cannot borrow as mutable because it is also borrowed as immutable as an obstacle and started reading it as a code review from a colleague who can see a bug I can't.
the fight I lost most often
Here's the canonical one, the loop that mutates the thing it's iterating:
let mut items = vec![1, 2, 3, 4];
for item in &items {
if *item % 2 == 0 {
items.push(*item * 10); // error: cannot borrow `items` as mutable
}
}
My first instinct was irritation. I can see what I mean. Other languages let me do this and only blow up at runtime if I'm unlucky. But sit with what the checker is objecting to. If the push reallocates the vector's backing storage, the iterator I'm holding is now pointing at freed memory, and the next read is undefined behaviour. The compiler isn't being fussy. It's refusing to let me write a use-after-free with a straight face.
The fix is to stop pretending I can read and write the same collection at the same time:
let mut items = vec![1, 2, 3, 4];
let new: Vec<i32> = items.iter()
.filter(|n| **n % 2 == 0)
.map(|n| n * 10)
.collect();
items.extend(new);
Two phases: decide what to add, then add it. The borrow on items ends when the iterator chain finishes, before extend takes its mutable borrow. The code is clearer too, which is the recurring theme. The version the checker forces on you is usually the version you'd have wanted anyway once you stopped being stubborn.
when the fight is real and the answer is shared ownership
Sometimes the checker isn't telling you your loop is wrong, it's telling you that the ownership model you've imagined doesn't actually exist. The classic is the graph or the observer list, where two things genuinely need to refer to the same object. You can fight that for a long time by passing references around and watching lifetimes metastasise across every function signature in the module.
The honest answer is that some data really is shared, and Rust has a vocabulary for it. Single-threaded shared ownership is Rc. If you also need to mutate through it, Rc<RefCell<T>>:
use std::rc::Rc;
use std::cell::RefCell;
type Shared<T> = Rc<RefCell<T>>;
let node: Shared<Node> = Rc::new(RefCell::new(Node::new()));
let also_node = Rc::clone(&node); // a second owner, refcounted
also_node.borrow_mut().value += 1; // mutate through the shared handle
This felt like cheating the first time, like I'd found the escape hatch and the checker had given up on me. It hasn't. RefCell moves the borrow checking from compile time to runtime: break the rules and you don't get a compiler error, you get a panic. The discipline is the same, you've just chosen to be checked later because the compiler genuinely couldn't prove your access pattern was safe ahead of time. That's a legitimate trade, not a defeat. The trick is reaching for it deliberately, when the data is actually shared, rather than reflexively, to make a borrow error go away.
the lifetimes I was inventing for no reason
A lot of my early pain was self-inflicted: I was storing references in structs when I should have been storing owned values. The moment a struct holds a &'a SomeThing, that lifetime annotation spreads to everything that touches the struct, and you spend your days threading 'a through signatures like wiring a house.
Most of the time I didn't need the reference at all. I wanted the struct to own its data. Replacing &str with String, or &[T] with Vec<T>, made entire lifetime parameters evaporate. Yes, you pay for an allocation. For the overwhelming majority of code that cost is irrelevant, and the readability you get back is enormous. Store references in structs when you've measured a reason to; otherwise, own your data and let the lifetimes look after themselves.
losing gracefully
The phrase "losing gracefully" is deliberate, because you do lose. You don't win against the borrow checker, you stop fighting it. The graceful version of losing looks like this: you read the error, you assume it's correct, you ask what real-world problem it's protecting you from, and you restructure so the answer is obvious rather than papering over it with a clone you didn't think about.
The ungraceful version, the one I started with, is sprinkling .clone() everywhere the moment something goes red, which compiles, runs, and quietly tells you nothing about your design. Clones aren't sinful and sometimes a clone is exactly right. But a clone added in frustration is a question you refused to answer, and the question was usually "who actually owns this?"
These days I genuinely don't fight it much, and the difference isn't that I've memorised the rules. It's that I've started designing with ownership in mind from the first line, asking who owns what before I type, so by the time the checker looks at my code we mostly agree already. The fights got rarer not because I got better at arguing but because I stopped writing code worth arguing about. That, it turns out, was the entire lesson, and the compiler was patient enough to keep teaching it until I listened.