I've been writing Rust on and off for a while now, since well before 1.0 calmed it down, and I still have weeks where the borrow checker and I have words. This was one of those weeks. The good news is that almost every argument I lost, I deserved to lose, and the code came out better for it. The bad news is that admitting that took me about four days.
This isn't a tutorial. If you've never seen Rust, none of this will mean much. This is for the people currently glaring at cannot borrow ... as mutable because it is also borrowed as immutable and wondering whether they've made a terrible career decision. You haven't. Probably.
The error everyone hits first
You have a vector. You want to iterate over it and, partway through, push something onto it. C would let you do that and occasionally segfault when the vector reallocated and your pointer went stale. Rust simply refuses.
let mut items = vec![1, 2, 3];
for item in &items {
if *item == 2 {
items.push(99); // error: cannot borrow `items` as mutable
}
}
The borrow checker is stopping you because the for loop holds an immutable borrow of items for its whole duration, and push needs a mutable one. You can't have both at once. The compiler isn't being pedantic. It's telling you that the thing you wrote has a genuine bug in any language, you'd be mutating a collection while iterating it, you've just never been forced to notice before.
The fix is to stop pretending the two operations are one. Collect what you want to add, then add it.
let mut items = vec![1, 2, 3];
let to_add: Vec<i32> = items.iter().filter(|&&x| x == 2).map(|_| 99).collect();
items.extend(to_add);
It's more lines. It's also correct, and it says out loud what's happening: first decide, then mutate. I've come to like that the language makes the phases explicit.
When you actually need shared mutable state
Sometimes the answer isn't "restructure", it's "you genuinely have shared mutable state and Rust has a tool for that, you've just been refusing to reach for it". The week's real wrestling match was a little graph where nodes needed to refer to each other.
My instinct, dragged in from every other language, was references everywhere. The borrow checker hated this with the heat of a thousand suns, because a graph with cycles has no single owner, and ownership is the entire model. After a day of lifetime may not live long enough I gave up trying to be clever and used the boring, correct tools.
use std::rc::Rc;
use std::cell::RefCell;
type Node = Rc<RefCell<NodeData>>;
struct NodeData {
value: i32,
edges: Vec<Node>,
}
Rc is a reference-counted pointer, so multiple nodes can share ownership of a node and it lives until the last one lets go. RefCell moves the borrow checking from compile time to runtime, so you can mutate through a shared reference, with the trade that if you break the borrowing rules it panics at runtime instead of failing to compile. That's a real cost. But it's the honest representation of "this is genuinely shared and genuinely mutable", and once I stopped fighting the model and used the escape hatch the model provides, it compiled and ran first try.
The one footgun worth flagging: Rc<RefCell<T>> cycles leak memory, because the reference count never reaches zero. If your graph has cycles and you want them collected, the back-edges need to be Weak<RefCell<T>> instead. I learned this by watching memory climb, naturally.
The mindset shift
Here's what finally made the week click. I'd been treating the borrow checker as an obstacle between me and the program I'd already written in my head. The shift was to treat it as a design conversation. Every time it pushed back, the question stopped being "how do I make this compile" and became "what is this telling me about ownership that I haven't thought through".
Most of the time the answer was that I had a genuine aliasing or lifetime problem I'd have shipped as a bug elsewhere. A handful of times the answer was "this is legitimately shared state, use Rc and RefCell and stop being a hero". Almost never was the answer "the borrow checker is wrong". I think over the whole week it was wrong zero times and I was wrong roughly constantly.
Losing gracefully
So that's the title. I lost. Repeatedly. And losing to the borrow checker turns out to be a productive way to spend a week, because every loss left behind code that won't data-race, won't use-after-free, and won't surprise me at three in the morning. The compiler did the worrying so I didn't have to.
I'm not going to pretend it's frictionless. There are days the fight isn't worth it and a garbage-collected language would have had me home an hour earlier. But for the things I'm building in Rust, a database-adjacent thing where correctness genuinely matters, that hour is cheap insurance. I'll keep losing these arguments. The trick is to lose them gracefully, take the lesson, and stop trying to out-clever a compiler that has thought about ownership far harder than I have at the end of a long week.