Rust Ownership and Borrowing: The Guide That Demystifies Everything
Master Rust ownership and borrowing with practical examples. Understand move semantics, references, lifetimes, and the borrow checker to write safe, efficient Rust code.

Rust ownership and borrowing form the foundation of Rust's memory safety guarantees. Unlike garbage-collected languages, Rust enforces strict rules at compile time through the borrow checker, eliminating entire classes of bugs -- null pointer dereferences, data races, and use-after-free errors -- without runtime overhead.
Every value in Rust has exactly one owner. When the owner goes out of scope, the value is dropped. Ownership can be transferred (moved) or temporarily lent (borrowed). These three rules replace garbage collection entirely.
How Move Semantics Replace Garbage Collection
Most languages let multiple variables point to the same heap-allocated data. Rust takes a different approach: assigning a heap value to another variable moves it, invalidating the original binding. The compiler enforces this at zero cost.
fn main() {
let original = String::from("interview prep");
let moved = original; // ownership transfers here
// println!("{}", original); // compile error: value moved
println!("{}", moved); // works fine
}This prevents double-free bugs. The String type allocates on the heap, so Rust ensures only one variable owns that allocation at any time. Stack-only types like i32 or bool implement the Copy trait and are duplicated instead of moved.
fn main() {
let x: i32 = 42;
let y = x; // copy, not move -- i32 is Copy
println!("x = {}, y = {}", x, y); // both valid
let s1 = String::from("hello");
let s2 = s1.clone(); // explicit deep copy
println!("s1 = {}, s2 = {}", s1, s2); // both valid after clone
}The distinction between Copy and Clone matters in interviews: Copy is implicit and cheap (bitwise), while Clone is explicit and can be expensive (heap allocation).
Borrowing with Immutable References
Transferring ownership everywhere would make code impractical. Rust borrowing solves this by lending access to a value without transferring ownership. An immutable reference (&T) allows read-only access, and multiple immutable references can coexist.
fn calculate_length(s: &String) -> usize {
s.len() // read access through the reference
} // s goes out of scope, but doesn't drop the String (not the owner)
fn main() {
let greeting = String::from("hello, Rust");
let len = calculate_length(&greeting); // borrow, don't move
println!("'{}' has {} characters", greeting, len); // greeting still valid
}The & symbol creates a reference that borrows the value. The function signature &String declares that calculate_length borrows without taking ownership. After the function returns, the caller retains full ownership.
At any given time, a value can have either: many immutable references (&T), OR exactly one mutable reference (&mut T). Never both simultaneously. This rule prevents data races at compile time.
Mutable References and the Exclusivity Rule
Mutable references (&mut T) grant write access but enforce exclusivity: only one mutable reference to a value can exist in a given scope. This prevents two pieces of code from modifying the same data simultaneously.
fn append_greeting(s: &mut String) {
s.push_str(", welcome to Rust!"); // modify through mutable ref
}
fn main() {
let mut message = String::from("Hello");
append_greeting(&mut message);
println!("{}", message); // "Hello, welcome to Rust!"
}The mut keyword appears in three places: the variable binding (let mut), the reference type (&mut), and the function parameter. All three are required. Attempting to create a second mutable reference in the same scope triggers a compile error.
fn main() {
let mut data = String::from("shared state");
let r1 = &mut data;
// let r2 = &mut data; // compile error: second mutable borrow
println!("{}", r1);
// After r1's last usage, a new mutable borrow is allowed
let r3 = &mut data; // this works -- non-lexical lifetimes
r3.push_str(" updated");
println!("{}", r3);
}Rust 2021 edition uses non-lexical lifetimes (NLL): a borrow ends at its last point of use, not at the end of the scope block. This makes the exclusivity rule more ergonomic without sacrificing safety.
Ready to ace your Rust interviews?
Practice with our interactive simulators, flashcards, and technical tests.
Lifetimes: Telling the Compiler How Long References Live
Lifetimes are Rust's way of ensuring references never outlive the data they point to. Most of the time, the compiler infers lifetimes automatically through lifetime elision rules. Explicit annotations become necessary when multiple references interact.
// 'a declares: the returned reference lives as long as
// the shortest-lived input reference
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let result;
let string1 = String::from("Rust ownership");
{
let string2 = String::from("borrowing");
result = longest(string1.as_str(), string2.as_str());
println!("Longest: {}", result); // valid: both strings alive
}
// println!("{}", result); // would fail: string2 dropped
}The 'a syntax is a lifetime parameter, not a new concept -- it annotates relationships that already exist. The function signature says: "the output reference cannot outlive either input reference." The compiler uses this to prevent dangling references.
Struct Borrowing and Lifetime Bounds
Structs that hold references must declare lifetime parameters. This ensures the struct cannot outlive the data it references -- a common source of dangling pointers in C/C++.
struct Excerpt<'a> {
text: &'a str, // this struct borrows a string slice
}
impl<'a> Excerpt<'a> {
fn summary(&self) -> &str {
let end = self.text.len().min(20);
&self.text[..end] // return a slice of the borrowed text
}
}
fn main() {
let article = String::from("Rust ownership model eliminates memory bugs");
let excerpt = Excerpt {
text: article.as_str(),
};
println!("Summary: {}", excerpt.summary());
}The lifetime 'a in Excerpt<'a> ties the struct's validity to the underlying string. Dropping article before excerpt would trigger a compile error.
Dangling reference questions appear frequently in Rust interviews. The answer is always: Rust prevents them at compile time through lifetime analysis. No runtime checks, no null pointers.
Ownership Patterns in Real-World Rust
Production Rust code relies on a few recurring ownership patterns. Recognizing them accelerates both development and interview performance.
// Pattern 1: Take ownership, return a new value
fn process_and_return(mut input: String) -> String {
input.push_str(" -- processed");
input // ownership transfers to caller
}
// Pattern 2: Borrow for read-only inspection
fn contains_keyword(text: &str, keyword: &str) -> bool {
text.to_lowercase().contains(&keyword.to_lowercase())
}
// Pattern 3: Borrow mutably for in-place modification
fn sanitize(input: &mut String) {
*input = input.trim().to_string();
}
fn main() {
// Pattern 1
let raw = String::from("user input");
let processed = process_and_return(raw);
// raw is now invalid, processed owns the data
// Pattern 2
let found = contains_keyword(&processed, "input");
println!("Contains 'input': {}", found);
// Pattern 3
let mut padded = String::from(" spaces everywhere ");
sanitize(&mut padded);
println!("Sanitized: '{}'", padded);
}Choosing between these patterns follows a simple heuristic: borrow immutably by default, borrow mutably when modification is needed, and transfer ownership only when the caller no longer needs the value. This approach is covered in depth in the Rust fundamentals guide.
Borrow Checker Errors and How to Fix Them
The borrow checker produces specific error codes. Understanding the most common ones turns frustrating compile errors into straightforward fixes.
fn main() {
// Error E0502: cannot borrow as mutable because also borrowed as immutable
let mut scores = vec![90, 85, 78];
// Fix: finish using the immutable borrow before mutating
let first = scores[0]; // copy (i32 is Copy), no active borrow
scores.push(95); // mutable borrow -- no conflict
println!("First: {}, All: {:?}", first, scores);
// Error E0382: use of moved value
let name = String::from("Alice");
let greeting = format!("Hello, {}", name); // format! borrows, doesn't move
println!("{} says {}", name, greeting); // both valid
// Error E0597: borrowed value does not live long enough
let outer;
{
let inner = String::from("temporary");
outer = inner; // move instead of borrow -- extends lifetime
}
println!("{}", outer); // works: outer owns the value
}Each fix follows the same principle: restructure the code so that borrows and ownership align with Rust's rules. Fighting the borrow checker usually signals a design issue that would cause bugs in other languages. For more advanced patterns involving concurrency and borrowing, shared ownership types like Arc and Mutex become essential.
Start practicing!
Test your knowledge with our interview simulators and technical tests.
Conclusion
- Every Rust value has one owner; ownership transfers on assignment (move semantics) unless the type implements
Copy - Immutable references (
&T) allow shared read access; mutable references (&mut T) enforce exclusive write access - The borrow checker prevents data races and dangling references at compile time with zero runtime cost
- Lifetimes annotate reference relationships -- they describe existing constraints, not new ones
- When the borrow checker rejects code, restructure the ownership flow rather than reaching for
unsafe - Practice these patterns with the Rust interview questions to build fluency before technical interviews
Tags
Share
Related articles

Ownership and Borrowing in Rust: Complete Guide
Master Rust's ownership and borrowing system. Understand property rules, references, lifetimes, and advanced memory management patterns.

Rust Interview Questions: Complete Guide 2026
The 25 most common Rust interview questions. Ownership, borrowing, lifetimes, traits, async and concurrency with detailed answers and code examples.

Rust: Basics for Experienced Developers in 2026
Learn Rust quickly by leveraging your existing knowledge. Ownership, borrowing, lifetimes, and essential patterns explained for developers coming from C++, Java, or Python.