Free Handbook · Every example compiled & verified

Ownership & Borrowing

Stack vs heap, the three ownership rules, moves, Clone vs Copy, references, the one-writer-or-many-readers rule, slices, and the borrow errors explained.

0 / 140 lessons🔥 0 day streak
ShareXLinkedIn

Module 03 · what you'll be able to do

  • Explain what lives on the stack and what lives on the heap, and why that matters for String but not for i32
  • State the three ownership rules and predict exactly where a value is moved and where it is dropped
  • Choose between moving, cloning, copying and borrowing when passing values to functions
  • Apply the rule "one mutable reference OR any number of shared references" and fix E0499 and E0502
  • Use &str and &[T] slices, and read E0382, E0596 and E0106 without panic
01

Stack vs heap

Ownership is the feature that makes Rust Rust: it is how the language frees memory without a garbage collector and without you calling free. To see why it exists, you need a picture of where values live.

Stack

  • Fixed-size values known at compile time: i32, f64, bool, char, tuples and arrays of those
  • Pushed when a function starts, popped when it returns — extremely fast
  • Each value has exactly one place; copying it is cheap

Heap

  • Data whose size is only known at runtime or that can grow: the text of a String, the elements of a Vec
  • Allocated on request, and must be freed exactly once
  • Reached through a pointer that lives on the stack

A String is really three stack values — a pointer to heap memory, a length and a capacity — plus the bytes on the heap. Somebody has to free those bytes exactly once. Free them too early and you get a use-after-free; twice and you get a double free; never and you leak. Ownership is the rule that says who frees them and when.

rustmain.rs
use std::mem::size_of;

fn main() {
    let n: i32 = 42;                        // all on the stack
    let mut s = String::with_capacity(16);  // pointer+len+cap on the stack, buffer on the heap
    s.push_str("hello");

    println!("i32 on the stack: {} bytes", size_of::<i32>());
    println!("String handle on the stack: {} bytes", size_of::<String>());
    println!("s: len {} capacity {}", s.len(), s.capacity());
    println!("{n} {s}");
}
Outputcompiled & run with real Rust
i32 on the stack: 4 bytes
String handle on the stack: 24 bytes
s: len 5 capacity 16
42 hello

On a 64-bit machine a String handle is always 24 bytes (three 8-byte words), however long the text is. The text itself lives on the heap.

02

The three ownership rules

  1. Each value in Rust has an owner — a variable (or a field, or a collection slot).
  2. There can be only one owner at a time.
  3. When the owner goes out of scope, the value is dropped: its destructor runs and its heap memory is freed.

Rule 3 is automatic: the compiler inserts the cleanup at the closing brace of the owner's scope. You can watch it happen by giving a type a Drop implementation that prints a line (traits are Module 07 — here it is just a hook that runs on drop).

rustmain.rs
struct Noisy(&'static str);

impl Drop for Noisy {
    fn drop(&mut self) {
        println!("drop {}", self.0);
    }
}

fn main() {
    let _a = Noisy("a");
    {
        let _b = Noisy("b");
        println!("inner scope ends");
    }                                   // _b dropped here
    let _c = Noisy("c");
    println!("main ends");
}                                       // _c then _a: reverse order of creation
Outputcompiled & run with real Rust
inner scope ends
drop b
main ends
drop c
drop a
Your turn

Add drop(_c); (the standard std::mem::drop) just before the last println!. Where does drop c appear now?

This is RAII
Tying cleanup to scope is called RAII (resource acquisition is initialisation), the same idea as C++ destructors. It is not only for memory: files close, locks unlock and network sockets shut when their owner is dropped. You never write a finally block in Rust.
03

Move semantics

What should let s2 = s1; do when s1 is a String? Copying the heap bytes would be slow and hidden. Copying only the pointer would leave two owners, and both would free the same buffer at the end of the scope — a double free. Rust does neither: it moves ownership. The stack handle is copied to s2, and s1 is marked as no longer valid. Only s2 will free the buffer.

VisualizeTracing a moveStep 1 / 6
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{s2}");
let s3 = s2;
println!("{s3}");
}
Line 2

s1 owns a heap buffer holding "hello".

Variables now
s1"hello" (owner)
All 6 steps as a table
StepLineWhat happenedVariables now
12s1 owns a heap buffer holding "hello".s1 = "hello" (owner)
23The pointer/len/capacity are copied into s2 and ownership moves with them. s1 is now invalid: the compiler rejects any later use of it.s1 = (moved) s2 = "hello" (owner)
34s2 is the owner, so reading it is fine.
45Ownership moves again. s2 is invalid too.s2 = (moved) s3 = "hello" (owner)
56Print through the current owner.
67End of scope. Only s3 is a live owner, so the buffer is freed exactly once. Moved-from variables drop nothing.
Error you will hit

E0382: borrow of moved value

rust
fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
    println!("{s1}, world");
    println!("{s2}");
}
error[E0382]: borrow of moved value: `s1`
 --> main.rs:4:16
  |
2 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 |     let s2 = s1;
  |              -- value moved here
4 |     println!("{s1}, world");
  |                ^^ value borrowed here after move
  |
help: consider cloning the value if the performance cost is acceptable
  |
3 |     let s2 = s1.clone();
  |                ++++++++
Why the compiler said that

The value moved from s1 to s2 on line 3. After that s1 owns nothing, so reading it on line 4 would be reading memory that s2 now controls. Read the three markers bottom-up: where it was used, where it moved, and why it moved (not Copy).

The fix

Decide what you meant. If both variables need their own text, clone. If you only need to read the value, borrow it (let s2 = &s1;). If you are done with s1, just use s2.

rust
fn main() {
    let s1 = String::from("hello");
    let s2 = &s1;          // borrow instead of move
    println!("{s1}, world");
    println!("{s2}");
}
rustmain.rs
fn main() {
    let names = vec![String::from("ada"), String::from("grace")];
    let team = names;                 // the whole Vec moves; no element is copied
    println!("{:?}", team);

    let mut owner = String::from("first");
    println!("{owner}");
    owner = String::from("second");   // old value is dropped, new one owned
    println!("{owner}");
}
Outputcompiled & run with real Rust
["ada", "grace"]
first
second
04

Clone vs Copy

.clone() makes a deep copy: a new heap allocation with the same contents, and a second independent owner. It is explicit on purpose — when you see clone() you know memory is being allocated. Small stack-only types are different: they implement the Copy trait, so assignment copies the bits and the original stays valid. There is nothing to free, so two copies cannot cause a double free.

rustmain.rs
fn main() {
    // Copy types: assignment duplicates, both stay usable
    let a = 5;
    let b = a;
    let p = (1.5, true);
    let q = p;
    println!("{a} {b} {:?} {:?}", p, q);

    // Clone: an explicit deep copy of heap data
    let s1 = String::from("hello");
    let mut s2 = s1.clone();
    s2.push_str(" world");
    println!("{s1} | {s2}");
}
Outputcompiled & run with real Rust
5 5 (1.5, true) (1.5, true)
hello | hello world
A type can be Copy only if it owns no resources that need cleanup.
Copy (assignment copies)Not Copy (assignment moves)
All integers, floats, bool, charString, Vec<T>, Box<T>, HashMap
Shared references &TMutable references &mut T
Tuples and arrays whose elements are all CopyTuples or arrays containing any non-Copy element
Your own types with #[derive(Clone, Copy)] (Module 04)Anything that implements Drop
Do not clone to silence the borrow checker
Adding .clone() makes most E0382 errors go away, which is why beginners sprinkle it everywhere. It is correct when you truly need two independent values. When you only need to read, borrowing is free and clone is wasted allocation. Reviewers notice.
05

Functions that take and return ownership

Passing a value to a function works exactly like assignment: a String moves into the parameter, an i32 is copied. When the function ends, its parameter goes out of scope and is dropped — unless the function hands it back as a return value, which moves ownership out to the caller.

rustmain.rs
fn take(s: String) {
    println!("took {s}");
}                                    // s dropped here: its heap memory is freed

fn make() -> String {
    String::from("fresh")            // ownership moves out to the caller
}

fn take_and_give_back(mut s: String) -> String {
    s.push('!');
    s                                // move it back out
}

fn main() {
    let a = String::from("one");
    take(a);                         // a moved in; a is invalid from here

    let n = 7;
    let doubled = double(n);         // i32 is Copy; n is still fine
    println!("{n} -> {doubled}");

    let b = make();
    let b = take_and_give_back(b);
    println!("{b}");
}

fn double(x: i32) -> i32 {
    x * 2
}
Outputcompiled & run with real Rust
took one
7 -> 14
fresh!
Error you will hit

E0382: using a value after passing it to a function

rust
fn shout(text: String) {
    println!("{}!", text.to_uppercase());
}

fn main() {
    let msg = String::from("ship it");
    shout(msg);
    println!("sent: {msg}");
}
error[E0382]: borrow of moved value: `msg`
 --> main.rs:8:22
  |
6 |     let msg = String::from("ship it");
  |         --- move occurs because `msg` has type `String`, which does not implement the `Copy` trait
7 |     shout(msg);
  |           --- value moved here
8 |     println!("sent: {msg}");
  |                      ^^^ value borrowed here after move
  |
note: consider changing this parameter type in function `shout` to borrow instead if owning the value isn't necessary
 --> main.rs:1:16
  |
1 | fn shout(text: String) {
  |    -----       ^^^^^^ this parameter takes ownership of the value
  |    |
  |    in this function
Why the compiler said that

shout declared its parameter as String, so calling it moved msg into the function, where it was dropped at the end. The compiler even suggests the real fix in its note: the function only reads the text, so it has no reason to own it.

The fix

Change the parameter to a borrow, &str, and pass &msg. Giving ownership back through the return value works too, but is clumsy — that clumsiness is exactly why references exist.

rust
fn shout(text: &str) {
    println!("{}!", text.to_uppercase());
}

fn main() {
    let msg = String::from("ship it");
    shout(&msg);
    println!("sent: {msg}");
}
06

References and borrowing

A reference &T lets you use a value without taking ownership of it. Creating one is called borrowing. The owner keeps the value, the borrower gets to look at it, and when the reference goes out of scope nothing is dropped — it never owned anything. References are guaranteed by the compiler to point at a valid value for as long as they exist.

rustmain.rs
fn count_vowels(text: &str) -> usize {
    text.chars().filter(|c| "aeiou".contains(*c)).count()
}

fn longest_len(words: &Vec<String>) -> usize {
    let mut best = 0;
    for w in words {                  // w is &String
        if w.len() > best {
            best = w.len();
        }
    }
    best
}

fn main() {
    let title = String::from("ownership and borrowing");
    let words = vec![String::from("move"), String::from("borrow"), String::from("slice")];

    println!("vowels: {}", count_vowels(&title));
    println!("longest: {}", longest_len(&words));
    println!("still mine: {title} / {:?}", words);   // nothing moved
}
Outputcompiled & run with real Rust
vowels: 7
longest: 6
still mine: ownership and borrowing / ["move", "borrow", "slice"]

A plain &T is a shared (read-only) reference. To change a borrowed value you need a mutable reference, &mut T. Both sides have to agree: the owner must be let mut, and the call site must write &mut, so mutation is visible where it happens.

rustmain.rs
fn add_bang(s: &mut String) {
    s.push('!');
}

fn reset(n: &mut i32) {
    *n = 0;              // * dereferences: write to the i32 itself
}

fn main() {
    let mut s = String::from("done");
    add_bang(&mut s);
    add_bang(&mut s);
    println!("{s}");

    let mut counter = 41;
    counter += 1;
    println!("{counter}");
    reset(&mut counter);
    println!("{counter}");
}
Outputcompiled & run with real Rust
done!!
42
0
Error you will hit

E0596: mutating through a shared reference

rust
fn add_bang(s: &String) {
    s.push('!');
}

fn main() {
    let mut s = String::from("done");
    add_bang(&s);
    println!("{s}");
}
error[E0596]: cannot borrow `*s` as mutable, as it is behind a `&` reference
 --> main.rs:2:5
  |
2 |     s.push('!');
  |     ^ `s` is a `&` reference, so it cannot be borrowed as mutable
  |
help: consider changing this to be a mutable reference
  |
1 | fn add_bang(s: &mut String) {
  |                 +++
Why the compiler said that

push changes the string, so it needs &mut String. The function only received &String, a read-only view. That s in main is mut does not matter: what a borrower may do is decided by the kind of reference it was given.

The fix

Take &mut String in the signature and pass &mut s at the call.

rust
fn add_bang(s: &mut String) {
    s.push('!');
}

fn main() {
    let mut s = String::from("done");
    add_bang(&mut s);
    println!("{s}");
}
07

One mutable XOR many shared

This is the rule the borrow checker enforces, and the one to memorise: at any moment a value may have either one mutable reference, or any number of shared references — never both. Readers can share; a writer needs to be alone. It is exactly the rule that prevents data races, and it also prevents a quieter bug: changing a collection while someone holds a pointer into it.

Error you will hit

E0499: two mutable borrows at once

rust
fn main() {
    let mut s = String::from("hi");
    let a = &mut s;
    let b = &mut s;
    a.push('!');
    b.push('?');
    println!("{s}");
}
error[E0499]: cannot borrow `s` as mutable more than once at a time
 --> main.rs:4:13
  |
3 |     let a = &mut s;
  |             ------ first mutable borrow occurs here
4 |     let b = &mut s;
  |             ^^^^^^ second mutable borrow occurs here
5 |     a.push('!');
  |     - first borrow later used here
Why the compiler said that

a is still going to be used on line 5, so its mutable borrow is alive when b tries to take a second one on line 4. Two writers to the same value at the same time is precisely what the rule forbids.

The fix

Finish with one mutable borrow before starting the next. Borrows end at their last use, not at the end of the scope, so reordering is often all it takes.

rust
fn main() {
    let mut s = String::from("hi");
    let a = &mut s;
    a.push('!');          // last use of a
    let b = &mut s;       // fine: a is finished
    b.push('?');
    println!("{s}");
}
Error you will hit

E0502: mutating while a shared borrow is alive

rust
fn main() {
    let mut scores = vec![10, 20, 30];
    let first = &scores[0];
    scores.push(40);
    println!("first = {first}");
}
error[E0502]: cannot borrow `scores` as mutable because it is also borrowed as immutable
 --> main.rs:4:5
  |
3 |     let first = &scores[0];
  |                  ------ immutable borrow occurs here
4 |     scores.push(40);
  |     ^^^^^^^^^^^^^^^ mutable borrow occurs here
5 |     println!("first = {first}");
  |                        ----- immutable borrow later used here
Why the compiler said that

This one is not pedantry. push may need more capacity, in which case the Vec allocates a new buffer, copies the elements and frees the old one. first would then point into freed memory. In C++ the same code compiles and is undefined behaviour ("iterator invalidation"); Rust refuses it.

The fix

Copy the value out (let first = scores[0]; — an i32 is Copy), or finish using the reference before mutating.

rust
fn main() {
    let mut scores = vec![10, 20, 30];
    let first = scores[0];     // a copy, not a borrow
    scores.push(40);
    println!("first = {first}");
}
rustmain.rs
fn main() {
    let mut data = vec![1, 2, 3];

    let r1 = &data;
    let r2 = &data;                       // many readers: fine
    println!("{:?} {:?}", r1, r2);        // last use of r1 and r2

    let w = &mut data;                    // one writer: fine, readers are done
    w.push(4);
    println!("{:?}", w);

    println!("len {}", data.len());       // w is done, the owner can read again
}
Outputcompiled & run with real Rust
[1, 2, 3] [1, 2, 3]
[1, 2, 3, 4]
len 4

Borrows last until their last use (non-lexical lifetimes), not until the closing brace. That is why the mutable borrow on line 8 is allowed.

You haveCan you also take &T?Can you also take &mut T?Can the owner read / write?
Only the ownerYesYes (owner must be mut)Yes / yes
One or more live &TYes, any numberNo — E0502Read yes, write no
One live &mut TNo — E0502No — E0499No — go through the reference
08

Dangling references and slices

A dangling reference points at memory that has already been freed. The classic way to create one is to return a reference to a local variable: the local is dropped when the function returns, and the caller is left holding a pointer to nothing. Rust rejects this at compile time.

Error you will hit

E0106: returning a reference to nothing

rust
fn dangle() -> &String {
    let s = String::from("temporary");
    &s
}

fn main() {
    println!("{}", dangle());
}
error[E0106]: missing lifetime specifier
 --> main.rs:1:16
  |
1 | fn dangle() -> &String {
  |                ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
  |
1 | fn dangle() -> &'static String {
  |                 +++++++
help: instead, you are more likely to want to return an owned value
  |
1 - fn dangle() -> &String {
1 + fn dangle() -> String {
  |
Why the compiler said that

A returned reference must borrow from something that outlives the call. This function has no reference parameters, so the only thing it could borrow from is its own local s — which is dropped at the closing brace. The help line says it plainly: "there is no value for it to be borrowed from". Lifetimes are the full story, in Module 08.

The fix

Return the owned String. Ownership moves out to the caller and nothing is freed early.

rust
fn no_dangle() -> String {
    String::from("temporary")
}

fn main() {
    println!("{}", no_dangle());
}

Slices: borrowing part of a collection

A slice is a reference to a contiguous run of elements: a pointer plus a length. &str is a slice of UTF-8 text; &[T] is a slice of an array or Vec. You make one with a range: &s[0..5], &v[2..], &v[..]. Because a slice is a borrow, the borrowing rules keep it valid — you cannot clear a string while a slice of it is still in use.

rustmain.rs
fn first_word(s: &str) -> &str {
    for (i, b) in s.bytes().enumerate() {
        if b == b' ' {
            return &s[..i];
        }
    }
    s
}

fn sum(values: &[i32]) -> i32 {
    let mut total = 0;
    for v in values {
        total += v;
    }
    total
}

fn main() {
    let owned = String::from("hello brave world");
    let literal = "single";                  // string literals are already &str
    println!("{}", first_word(&owned));      // &String coerces to &str
    println!("{}", first_word(literal));
    println!("{}", &owned[6..11]);

    let nums = vec![4, 8, 15, 16, 23, 42];
    let arr = [1, 2, 3];
    println!("{} {} {}", sum(&nums), sum(&nums[1..3]), sum(&arr));
}
Outputcompiled & run with real Rust
hello
single
brave
108 23 6
Your turn

Write fn largest(values: &[i32]) -> i32 and call it with the whole nums vector and with the slice &nums[..3].

Take &str and &[T] in function parameters
A parameter typed &str accepts a string literal, a &String and a slice of either. A parameter typed &String accepts only the second. The same goes for &[T] over &Vec<T>. Prefer the slice type — Clippy even warns about &Vec<T> parameters.
String slice indexes are byte offsets
&s[0..2] counts bytes, not characters. Slicing through the middle of a multi-byte character such as é panics with byte index 1 is not a char boundary. Use chars() when you need characters (Module 05).
Owner
The variable responsible for a value. When it goes out of scope, the value is dropped.
Move
Transferring ownership by assignment, passing or returning. The source can no longer be used.
Copy
A trait for small stack-only types; assignment duplicates the bits and the source stays valid.
Clone
An explicit deep copy, usually a new heap allocation: .clone().
Drop
Freeing a value when its owner goes out of scope. Happens in reverse order of declaration.
Borrow
Creating a reference to a value without taking ownership.
Shared reference
&T: read-only access. Any number may exist at once.
Mutable reference
&mut T: read-write access. Only one may exist, with no shared references alongside.
Borrow checker
The compiler pass that enforces the ownership and borrowing rules.
Slice
A reference to a contiguous part of a collection: &str, &[T].
Dangling reference
A pointer to memory that has been freed. Impossible in safe Rust.
Quick check

let a = String::from("x"); let b = a; let c = 5; let d = c; — which variables can still be used afterwards?

Quick check

While a shared reference r = &v[0] is still going to be used, you call v.push(1). What happens?

Frequently asked questions

What is ownership in Rust, in one sentence?
Every value has exactly one owner variable, and when that owner goes out of scope the value is freed — so memory is managed at compile time with no garbage collector and no manual free.
When should I use clone() in Rust?
When you genuinely need two independent copies of heap data, for example to keep an original while modifying a copy. If you only need to read a value, pass a reference (&T) instead; it costs nothing.
Why can I not have two mutable references to the same value in Rust?
Two writers to the same memory at once is a data race in threaded code and a source of invalidated pointers in single-threaded code. Rust rules it out at compile time: one &mut T or any number of &T, never both.

Finish the Rust handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.