Skip to content
Vladimir Chavkov
Go back

Rust Ownership and Borrowing: The Mental Model You Need

Edit page

If you’re coming from Python, JavaScript, or Go, Rust’s ownership system is probably the single biggest conceptual hurdle you’ll face. The good news: once the mental model clicks, it becomes second nature. This post breaks it down without the academic jargon.

The Three Ownership Rules

Every value in Rust has exactly one owner. That’s the core principle. From it, three rules follow:

  1. Each value has a single owner (a variable binding).
  2. When the owner goes out of scope, the value is dropped (freed).
  3. Ownership can be moved to another variable, but the original can no longer be used.

Here’s what “move” looks like in practice:

let name = String::from("Alice");
let greeting = name; // ownership moved to `greeting`
// println!("{}", name); // COMPILE ERROR: value used after move
println!("{}", greeting); // works fine

In Python or JS, both variables would point to the same string. In Rust, name is invalidated after the move. This prevents double-free bugs and data races at compile time.

Borrowing: References Without Ownership

Moving ownership everywhere would be impractical. Borrowing lets you access data without taking ownership. There are two kinds:

Shared references (&T) — read-only, many allowed

fn print_length(s: &String) {
println!("Length: {}", s.len());
}
let name = String::from("Alice");
print_length(&name); // borrow name
println!("{}", name); // still valid -- we only borrowed

Mutable references (&mut T) — read-write, only one at a time

fn append_greeting(s: &mut String) {
s.push_str(", hello!");
}
let mut name = String::from("Alice");
append_greeting(&mut name);
println!("{}", name); // "Alice, hello!"

The key constraint: you can have either multiple &T or one &mut T, but never both simultaneously. This rule eliminates data races at compile time.

Common Compile Errors and Fixes

Error: cannot borrow as mutable more than once

let mut data = vec![1, 2, 3];
let a = &mut data;
let b = &mut data; // ERROR: second mutable borrow
a.push(4);

Fix: Ensure the first mutable borrow’s scope ends before creating another:

let mut data = vec![1, 2, 3];
{
let a = &mut data;
a.push(4);
} // `a` goes out of scope here
let b = &mut data; // now this is fine
b.push(5);

Error: cannot borrow as mutable because it is also borrowed as immutable

let mut scores = vec![90, 85, 70];
let first = &scores[0]; // immutable borrow
scores.push(100); // ERROR: mutable borrow while immutable exists
println!("{}", first);

Fix: Use the immutable reference before mutating:

let mut scores = vec![90, 85, 70];
let first = scores[0]; // copy the value instead of borrowing
scores.push(100); // no conflict
println!("{}", first);

Lifetimes: The Basics

Lifetimes ensure references don’t outlive the data they point to. Most of the time, Rust infers them automatically. You only write explicit lifetimes when the compiler can’t figure out the relationship:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}

The 'a annotation tells the compiler: “the returned reference lives at least as long as both inputs.” Without it, the compiler wouldn’t know which input the return value borrows from.

Rule of thumb: if the compiler asks for lifetime annotations, it’s telling you that the relationship between input and output references is ambiguous. The annotation resolves the ambiguity.

Why Does the Borrow Checker Exist?

Languages like C give you full control over memory but let you create dangling pointers, double frees, and data races. Languages like Python and Go use garbage collection, trading runtime performance for safety. Rust’s borrow checker gives you both: zero-cost memory safety verified at compile time.

Every time the borrow checker rejects your code, it caught a bug that would have been a segfault in C or a race condition in Go.

Practical Patterns to Adopt

The ownership system is not a punishment. It’s a contract: if your code compiles, an entire category of bugs simply cannot exist at runtime. That’s a trade worth making.


Edit page
Share this post on:

Previous Post
Rust Web Development: Getting Started with Actix Web and Axum
Next Post
Building Production REST APIs with Go