Updated Aug 6, 2026

05. Ownership and Borrowing

📋 Jump to Takeaways

🎁 You assign a variable to another one and the original stops working. No runtime crash, no null pointer, the compiler flat-out refuses to compile. Why would a language do this on purpose?

The Three Ownership Rules

Rust's memory safety comes from three rules enforced at compile time:

  1. Each value has exactly one owner (a variable).
  2. When ownership is transferred (assigned or passed to a function), the original variable is invalidated.
  3. When the owner goes out of scope, the value is dropped (memory freed).
fn main() {
    let s = String::from("hello"); // s owns the String
    println!("{}", s);             // hello
} // s goes out of scope — memory is freed automatically

No garbage collector. No manual free(). The compiler inserts cleanup code at the exact right point.

Move Semantics

When you assign one variable to another, Rust moves ownership by default. The original becomes invalid. Types that live entirely on the stack are an exception — they implement Copy and are cheaply duplicated instead of moved. This includes all integers (i8i128, u8u128, isize, usize), all floats (f32, f64), bool, char, and &str (a pointer + length, both stack values).

fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // s1 is MOVED to s2

    println!("{}", s1); // ❌ ERROR: value borrowed after move
    println!("{}", s2); // hello
}

The compiler error:

error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:4:20
  |
2 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`
3 |     let s2 = s1;
  |              -- value moved here
4 |     println!("{}", s1);
  |                    ^^ value borrowed here after move

This isn't a bug, it's the design. Two variables can't own the same heap data because then who frees it? Rust eliminates double-free by making moves the default.

Why Moves Exist

A String is stored as a pointer, length, and capacity on the stack, pointing to data on the heap. If Rust just copied the stack data (like C would), both s1 and s2 would point to the same heap memory. When both go out of scope, the memory gets freed twice, a double-free bug.

Rust's solution: after let s2 = s1, the compiler considers s1 invalid. One owner, one free, zero bugs.

Clone: Explicit Deep Copy

When you actually need a copy, call .clone() to explicitly duplicate the heap data.

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone(); // deep copy — both are valid

    println!("s1: {}", s1); // s1: hello
    println!("s2: {}", s2); // s2: hello
}

.clone() is intentionally verbose. It signals "this allocates memory" so you can spot performance costs at a glance.

Not all types can be cloned. Only types that implement the Clone trait. File handles, mutex locks, and network sockets can't be cloned because duplicating them doesn't make sense. If a type contains a non-Clone field, it can't be Clone either.

The Copy Trait: Stack Types Are Different

Simple types that live entirely on the stack implement the Copy trait. Assignment copies them instead of moving.

fn main() {
    let x = 5;
    let y = x; // copy, not move — both valid

    println!("x: {}, y: {}", x, y); // x: 5, y: 5
}

Types that implement Copy: all integers (i8i128, u8u128, isize, usize), all floats (f32, f64), bool, char, &str, and tuples where every element is Copy. One non-Copy field poisons the whole tuple:

fn main() {
    let a = (1, 2, true);           // all Copy → tuple is Copy
    let b = a;
    println!("{:?}", a);            // (1, 2, true) — works, a was copied

    let c = (1, String::from("hi")); // String is NOT Copy → tuple moves
    let d = c;
    println!("{:?}", c);            // ❌ error, c was moved
    println!("{:?}", d);            // (1, "hi")
}

Structs follow the same rule — all fields must be Copy, and you must explicitly opt in with #[derive(Copy, Clone)]:

#[derive(Copy, Clone)]
struct Point {
    x: f64,
    y: f64,
}

let p1 = Point { x: 1.0, y: 2.0 };
let p2 = p1; // copied, not moved
println!("{}", p1.x); // ✅ still valid

One non-Copy field and the whole struct can't be Copy:

#[derive(Copy, Clone)] // ❌ won't compile — String is not Copy
struct User {
    id: u32,
    name: String,
}

Unlike tuples where Copy is automatic when all elements qualify, structs require the explicit derive — the compiler won't add it silently. String, Vec, and anything that allocates on the heap does NOT implement Copy.

&str is a special case — it's Copy because it's just a pointer + length on the stack. Copying a &str copies the pointer, not the string data behind it. The data itself lives in static memory (baked into the binary at compile time), not the stack or heap:

let s: &str = "hello";
let s2 = s;
println!("{}", s);  // ✅ works — &str is Copy
println!("{}", s2); // ✅
Type Data lives Copy?
&str static memory (binary)
String heap
i32, bool, etc. stack

Copy is a subtrait of Clone, so every Copy type can also call .clone(). But you'd never do it — assignment already copies them implicitly. The hierarchy:

  • Copy — implicit bitwise copy on assignment (i32, bool, &str, ...)
  • Clone (without Copy) — explicit .clone() call, may allocate (String, Vec)
  • Neither — can't be duplicated at all (File, MutexGuard, TcpStream)

Ownership and Functions

Passing a value to a function moves it, just like assignment. The function takes ownership.

fn take_ownership(s: String) {
    println!("{}", s);
} // s is dropped here

fn main() {
    let greeting = String::from("hi");
    take_ownership(greeting);

    // println!("{}", greeting); // ❌ ERROR: greeting was moved
}

After calling take_ownership, you can't use greeting anymore. The function consumed it.

References: Borrowing Without Ownership

What if you want a function to read a value without taking it? Use a reference (&). This is called borrowing.

fn calculate_length(s: &String) -> usize {
    s.len()
} // s goes out of scope, but since it doesn't own the String, nothing is dropped

fn main() {
    let greeting = String::from("hello");
    let len = calculate_length(&greeting); // borrow, don't move

    println!("'{}' has length {}", greeting, len);
    // 'hello' has length 5
}

The & creates a reference that borrows the value. You can use greeting afterwards because ownership never transferred.

Mutable References

By default, references are immutable, you can look but not touch. To modify borrowed data, use &mut. This is still borrowing, not ownership transfer. The caller keeps ownership, the function just gets temporary permission to modify.

fn add_world(s: &mut String) {
    s.push_str(", world!");
} // borrow ends here — caller still owns the String

fn main() {
    let mut greeting = String::from("hello");
    add_world(&mut greeting); // lend it mutably

    println!("{}", greeting); // hello, world! — still ours
}

Compare the three ways a function can receive a value:

fn takes_ownership(s: String) {}   // owner transfers, caller loses it
fn borrows(s: &String) {}          // read-only loan, caller keeps it
fn borrows_mut(s: &mut String) {}  // writable loan, caller keeps it

The variable must be declared mut, and the reference must be &mut. Both sides agree to the mutation.

If the variable isn't declared mut, you can't create a &mut reference to it at all:

fn main() {
    let s = String::from("hello"); // not mut
    let r = &mut s; // ❌ ERROR: cannot borrow `s` as mutable, as it is not declared as mutable
}

The mut keyword on the binding is what grants permission to mutate. Without it, the value is frozen through every path, whether direct assignment or mutable reference.

The One Mutable Reference Rule

Rust enforces: you can have either one mutable reference OR any number of immutable references, never both at the same time.

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

    let r1 = &s;     // OK — immutable borrow
    let r2 = &s;     // OK — another immutable borrow
    println!("{} {}", r1, r2); // hello hello

    let r3 = &mut s; // OK — r1 and r2 are no longer used after this point
    r3.push_str("!");
    println!("{}", r3); // hello!
}

But this fails:

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

    let r1 = &s;
    let r2 = &mut s; // ❌ ERROR: cannot borrow as mutable because it's also borrowed as immutable

    println!("{}", r1);
}
error[E0502]: cannot borrow `s` as mutable because it is also
              borrowed as immutable

This fails because r1 is still used after r2 is created. The println!("{}", r1) keeps the immutable borrow alive, so both borrows overlap. Remove that line and it compiles fine. Rust tracks when a borrow is last used, not where it's declared. A borrow ends at its last use, so two borrows can coexist as long as they don't overlap.

This rule prevents data races at compile time. If someone is reading, nobody can write. If someone is writing, nobody else can read or write.

Dangling References: Impossible in Rust

In C/C++, you can return a pointer to local data that gets freed, a dangling pointer. Rust won't let you:

// This does NOT compile
fn dangle() -> &String {
    let s = String::from("hello");
    &s // ❌ ERROR: s is dropped at end of function, reference would dangle
}
error[E0106]: missing lifetime specifier

The fix: return the owned value instead.

fn no_dangle() -> String {
    let s = String::from("hello");
    s // move ownership out — no dangling reference
}

fn main() {
    let s = no_dangle();
    println!("{}", s); // hello
}

Rust guarantees at compile time that references always point to valid data.

The Borrow Checker Summary

The borrow checker enforces these rules every time you compile:

  • A value has exactly one owner
  • A value is dropped when its owner leaves scope
  • You can have unlimited &T (shared/immutable references) OR exactly one &mut T (exclusive/mutable reference), not both
  • References must always be valid (no dangling)

These rules eliminate: use-after-free, double-free, data races, and dangling pointers, all at compile time with zero runtime cost. (Null safety comes separately from the type system, Rust has no null, only Option<T>.)

Key Takeaways

  • Each value has one owner; when the owner goes out of scope, the value is dropped
  • Assigning a String to another variable moves it, the original is invalidated
  • Use .clone() for explicit deep copies when you need both variables alive
  • Stack-only types (i32, bool, char) implement Copy and are duplicated on assignment
  • &T borrows without ownership, the original owner keeps the value
  • &mut T allows modification, but only one mutable reference can exist at a time
  • You cannot mix &T and &mut T references to the same data simultaneously
  • The compiler prevents dangling references, return owned values instead
  • All of this is checked at compile time with zero runtime overhead

🎁 You can group related data and behavior together in Rust by building your own types. Next lesson: Structs and Methods. You'll define a User with fields, attach methods with impl, and build constructors using associated functions.

📝 Ready to test your knowledge?

Answer the quiz below to mark this lesson complete.

Spot something off? Report an issue
© 2026 ByteLearn.dev. Free courses for developers. · Privacy