Ramblings of an aging IT geek
← Ramblings of an aging IT geek
rust

the borrow checker was right and i was wrong, again

A walk through the borrow checker errors that kept stopping me on a small Rust project, and what each one was actually telling me.

A terminal showing Rust source code

There is a stage in learning Rust where you are in a fight with the borrow checker, and you are certain you are winning, and you are not. I was in that stage for about a month last year. I am out of it now, mostly, and the way out turned out to be admitting that every single time the borrow checker stopped me, it was correct and I was the problem.

The project was nothing special. A little tool that reads a config file, builds an in-memory index of some records, and lets you query it. The kind of thing you would write in Python before your tea went cold. In Rust it took me three evenings, and two of those evenings were spent staring at cannot borrow as mutable because it is also borrowed as immutable.

The shape of my mistake was always the same. I would be iterating over a collection and, partway through the loop, decide to modify that same collection. In Python you do this without thinking and occasionally get a surprise. In Rust the compiler simply refuses, and for a while I read that refusal as the language being precious.

for record in &records {
    if record.stale() {
        records.remove(record.id); // no.
    }
}

The error here is not pedantry. If you removed an item mid-iteration, the iterator could be pointing at freed memory or a shifted index, and that is exactly the class of bug that turns into a heisenbug in a language that lets you do it. The borrow checker is not stopping me from doing something sensible. It is stopping me from doing something that only looks sensible until the collection reallocates under my feet.

A terminal listing compiler errors and a fixed loop

The losing-gracefully part is the bit I want to recommend. Once I stopped trying to out-argue the compiler and started reading the error as a hint about my design, the fixes were usually small and usually made the code better. The loop above became "collect the ids to remove, then remove them," which is two passes and a clearer statement of intent. Where I genuinely needed shared mutable state I reached for the right tool, a RefCell here, an index-based loop there, rather than fighting to keep a borrow alive past its welcome.

The lifetime errors were harder, and I will not pretend I understood them quickly. The breakthrough was realising that most of my lifetime pain came from trying to hold references in structs when I should have been holding owned values. A struct that borrows from something else is a struct with a leash, and the leash has to be shorter than whatever it is tied to. Half my errors vanished the day I started cloning a String instead of clinging to a &str to save an allocation that did not matter in a tool that runs once and exits.

What I would tell my month-ago self is this: the borrow checker is not an obstacle between you and your program. It is a reviewer who has already read your code and found the bug you have not hit yet. When it stops you, the useful question is not "how do I make this compile" but "what is it seeing that I am not." Nine times in ten there really is something. The tenth time you reach for clone(), ship it, and get on with your life. Losing gracefully is just that: stop arguing, take the hint, and let the compiler have been right.