Academy
Common Ownership Pitfalls: The Mistakes Every Rust Newcomer Makes
Master Rust's borrow checker by learning how to avoid common ownership pitfalls like moving used values and mixing mutable and immutable borrows.
6 min read
On this page
- Pitfall 1: Using a Value After Moving It
- Pitfall 2: Mixing Mutable and Immutable Borrows
- Pitfall 3: Returning References to Local Data
- Pitfall 4: Lifetime Mismatches
- Pitfall 5: Iterating and Modifying Simultaneously
- The Meta-Pattern: When the Compiler Says No
- Broader Implications: Pitfalls That Become Intuition
- Practical Applications & Strategic Takeaways
- Our Commitment to Open Knowledge
- Support Our Mission & Get Your Handbook
There's a phase in every Rust developer's journey that the community affectionately calls "fighting the borrow checker." It's the period—usually the first few weeks—when the compiler rejects code that seems perfectly reasonable, when error messages about moved values and conflicting borrows fill your terminal, and when you seriously consider whether Python was such a bad choice after all.
Here's the secret that experienced Rust developers know: you're not fighting the borrow checker. The borrow checker is fighting your habits from other languages. The code it rejects isn't "perfectly reasonable"—it has bugs that other languages would let through. The moved value error? That's a use-after-free that C would happily compile. The conflicting borrows error? That's a data race that Java's runtime would catch (maybe) as a ConcurrentModificationException (if you're lucky). The borrow checker isn't being pedantic—it's catching real bugs.
Once you understand the common patterns that trigger these errors—and more importantly, the correct patterns that replace them—the borrow checker transforms from an adversary into an ally. It becomes the fastest code reviewer on your team, catching in milliseconds what would otherwise cost hours of debugging.
At RantAI, we've onboarded dozens of engineers to Rust. Every one of them hit the same pitfalls. Every one of them got past them. This article, drawn from Chapter 9, Section 9.3 of our guide "The Rust Programming Language," documents the most common ownership pitfalls and their solutions.
Pitfall 1: Using a Value After Moving It#
// THE MISTAKE
fn main() {
let name = String::from("Alice");
let greeting = format!("Hello, {}!", name);
let farewell = format!("Goodbye, {}!", name); // Works! format! borrows, doesn't move
let names = vec![name]; // name is MOVED into the vector
println!("{}", name); // COMPILE ERROR: name was moved
}
Why it happens: In most languages, names.push(name) copies name. In Rust, it moves name into the vector. The original variable is invalidated.
The fix: Clone if you need both, or restructure to avoid the move:
// FIX 1: Clone before moving
let names = vec![name.clone()]; // Clone creates an independent copy
println!("{}", name); // Original still valid
// FIX 2: Use references instead of owned values
let names: Vec<&str> = vec![&name]; // Borrow instead of move
println!("{}", name); // Original still valid
// FIX 3: Restructure to use the value after the move isn't needed
let name = String::from("Alice");
println!("{}", name); // Use first
let names = vec![name]; // Move last
Pitfall 2: Mixing Mutable and Immutable Borrows#
// THE MISTAKE
fn main() {
let mut data = vec![1, 2, 3, 4, 5];
let first = &data[0]; // Immutable borrow
data.push(6); // Mutable borrow (push modifies the vec)
println!("{}", first); // COMPILE ERROR: first might be invalidated
}
Why it happens: data.push(6) might reallocate the vector's memory (if it needs to grow), which would invalidate the reference first. The compiler prevents this even if the reallocation wouldn't actually happen in this specific case—because it prevents it in every case.
The fix:
// FIX 1: Use the immutable borrow before the mutable one
let mut data = vec![1, 2, 3, 4, 5];
let first = data[0]; // Copy the value (i32 implements Copy)
data.push(6); // Now safe to modify
println!("{}", first); // first is an independent copy
// FIX 2: Scope the borrows
let mut data = vec![1, 2, 3, 4, 5];
{
let first = &data[0];
println!("{}", first); // Use and drop the immutable borrow
}
data.push(6); // Now safe — no outstanding borrows
Pitfall 3: Returning References to Local Data#
// THE MISTAKE
fn create_greeting(name: &str) -> &str {
let greeting = format!("Hello, {}!", name);
&greeting // COMPILE ERROR: greeting is dropped at end of function
}
Why it happens: The String created by format! is owned by the function. When the function returns, the String is dropped. The reference would point to freed memory.
The fix: Return the owned value instead:
// FIX: Return the owned String
fn create_greeting(name: &str) -> String {
format!("Hello, {}!", name) // Ownership transfers to caller
}
Pitfall 4: Lifetime Mismatches#
// THE MISTAKE
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
if x.len() > y.len() {
x
} else {
y // COMPILE ERROR: y doesn't have lifetime 'a
}
}
Why it happens: The function says the return value lives as long as x (lifetime 'a), but it might return y, which could have a shorter lifetime.
The fix: Give both parameters the same lifetime:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
Now the return value's lifetime is the shorter of the two inputs' lifetimes, which is always safe.
Pitfall 5: Iterating and Modifying Simultaneously#
// THE MISTAKE
fn remove_negatives(data: &mut Vec<i32>) {
for (i, &val) in data.iter().enumerate() { // Immutable borrow via iter()
if val < 0 {
data.remove(i); // COMPILE ERROR: can't mutably borrow during iteration
}
}
}
The fix: Use retain, which is designed for this pattern:
fn remove_negatives(data: &mut Vec<i32>) {
data.retain(|&val| val >= 0); // Idiomatic and safe
}
The Meta-Pattern: When the Compiler Says No#
Every borrow checker error follows one pattern: you're trying to create an aliased mutable reference. Either two variables point to the same data and one is mutable (data race risk), or a reference outlives its data (dangling reference risk). The fix always involves one of:
Restructure: Reorder operations so borrows don't overlap
Clone: Create independent copies when sharing is needed
Use interior mutability:
RefCell<T>orMutex<T>for runtime-checked borrowingChange the API: Return owned values instead of references
Broader Implications: Pitfalls That Become Intuition#
At RantAI, new engineers hit these pitfalls in their first week. By their third week, they've internalized the patterns and write code that compiles on the first try. The borrow checker hasn't gotten easier—their mental model of ownership has become accurate. They think about "who owns this data?" and "how long does this reference need to live?" naturally, and the code they write reflects that thinking.
Practical Applications & Strategic Takeaways#
For newcomers: Every borrow checker error is a bug that other languages would let through. Read the error message carefully—Rust's error messages often suggest the exact fix.
For teams: Share a "common ownership patterns" document. The same pitfalls appear in every Rust codebase, and the same fixes apply.
For architects: If your functions require complex lifetime annotations, your ownership boundaries might be in the wrong place. Simplify by moving ownership transfers to function boundaries.
Our Commitment to Open Knowledge#
RantAI is committed to open education. Common ownership pitfalls are covered in Chapter 9, Section 9.3 of our guide, "The Rust Programming Language," freely available online.
Explore these concepts further: https://trpl.rantai.dev
Support Our Mission & Get Your Handbook#
Get the Handbook on Amazon KDP: https://www.amazon.com/dp/B0DHCMD3F2
Get the Handbook on Google Play Books: https://play.google.com/store/books/details?id=INwfEQAAQBAJ
What was your most frustrating borrow checker moment—and what did you learn from it? Share your war stories below!
#RustLang #BorrowChecker #OwnershipPitfalls #CompilerErrors #RantAI #LearnRust #Debugging #MemorySafety #CommonMistakes #SystemsProgramming
Next article
Borrowing: How Rust Lets You Look Without Touching