Lesson 3 of 12

Ownership & Borrowing

Learn the ownership and borrowing rules that let Rust manage without a garbage collector. This is the most important new idea in Rust, so take it one step at a time.

Every value in Rust has exactly one owner. When the owner goes out of , the value is dropped (freed). No garbage collector. No manual free(). This is Rust's entire memory safety story.

Rust
fn main() {
    // Ownership basics
    let s1 = String::from("hello");
    let s2 = s1;  // s1 is MOVED to s2
    // println!("{}", s1);  // ERROR: s1's value moved to s2

    // Clone — explicit deep copy
    let s1 = String::from("hello");
    let s2 = s1.clone();   // s1 is still valid
    println!("{} {}", s1, s2);

    // Copy types (integers, floats, bool, char, tuples of copies)
    // are copied, not moved
    let x = 5;
    let y = x;   // x is COPIED, not moved
    println!("{} {}", x, y);   // both work fine

    // Move into a function
    let s = String::from("hello");
    takes_ownership(s);     // s is moved
    // println!("{}", s);   // ERROR: s was moved

    // Return ownership
    let s = gives_ownership();
    println!("{}", s);      // works
}

fn takes_ownership(s: String) {
    println!("{}", s);
}   // s dropped here, memory freed

fn gives_ownership() -> String {
    String::from("yours now")
}

References let you use a value without taking ownership. A reference is like a pointer, but the borrow checker guarantees it's always valid — no dangling pointers ever.

Rust
fn main() {
    let s = String::from("hello");

    // Shared reference — borrow without taking ownership
    let len = calculate_len(&s);   // &s is a reference to s
    println!("{} has length {}", s, len);   // s still works!

    // Mutable reference — allows mutation
    let mut s = String::from("hello");
    change(&mut s);
    println!("{}", s);   // hello world

    // Rules:
    // 1. Any number of shared (&) references, OR
    // 2. Exactly ONE mutable (&mut) reference — never both
    // These rules prevent data races at compile time.

    let r1 = &s;
    let r2 = &s;
    println!("{} and {}", r1, r2);  // OK — two shared refs
    // let r3 = &mut s;            // ERROR — can't have mutable while shared refs exist
}

fn calculate_len(s: &String) -> usize {
    s.len()
}   // s is NOT dropped, we only borrowed it

fn change(s: &mut String) {
    s.push_str(" world");
}

Common question

Is the borrow checker something to work around?

Ownership is Rust's answer to the question every systems language has to answer: when does this memory get released? Each value has exactly one owner, and when the owner goes out of scope the value is dropped. Passing a value moves ownership; borrowing lends access without handing it over, under rules that forbid a borrow alongside any other.

The checks these rules before the program runs, so the errors show up while you type instead of in production. Treat them as design questions. Borrow for reading, move for handing off, and use lifetimes to say how long a reference is valid. What feels like friction at first becomes the reason a Rust program that compiles usually works.

Source: The Rust Book sums up the rules: each value has one owner; you can have many shared references or one reference, never both; and a reference must never outlive the value it points to.

Common mix-ups

Each of these sounds reasonable. Think about why it is wrong, then open the explanation.

  • The compiler rejects programs that would run fine, so it's too strict.

  • Cloning everything is the practical way to keep the checker happy.

  • Garbage collection would solve the same problems more simply.

Finished reading?Mark it done to track your progress.

Tip: use the ← and → keys to move between lessons.