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 memory management visualization

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.

The Three Rules of Ownership

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.

move_semantics.rsrust
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.

copy_vs_move.rsrust
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.

immutable_borrowing.rsrust
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.

Borrowing Rules at a Glance

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.

mutable_borrowing.rsrust
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.

exclusivity_rule.rsrust
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.

lifetime_annotation.rsrust
// '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_lifetime.rsrust
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.

Common Interview Pitfall

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.

ownership_patterns.rsrust
// 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.

common_fixes.rsrust
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

#rust
#ownership
#borrowing
#memory-management
#systems-programming

Share

Related articles