Ownership & Borrowing
Ownership is Rust's most unique feature and has deep implications for the rest of the language. It enables Rust to make memory safety guarantees without needing a garbage collector.
What is Ownership?
Ownership is a set of rules that govern how a Rust program manages memory. Unlike languages with garbage collection (like Java or Python) or manual memory management (like C), Rust uses a third approach: memory is managed through a system of ownership with rules that the compiler checks at compile time.
The Three Rules of Ownership
- Each value in Rust has an owner — usually a variable, but a struct field or a collection element owns its value just as well
- There can only be one owner at a time
- When the owner goes out of scope, the value will be dropped
fn main() {
// s comes into scope
let s = String::from("hello");
// s is valid from this point forward
println!("{}", s);
} // s goes out of scope and is dropped
The Stack and the Heap
To understand ownership, we need to understand how data is stored:
- Stack: Fast, fixed-size data (integers, booleans, references)
- Heap: Dynamic-size data (Strings, Vectors, etc.)
fn main() {
// i32 is stored on the stack - copy is cheap
let x = 5;
let y = x; // y is a copy of x
println!("x = {}, y = {}", x, y); // Both are valid!
// String is stored on the heap
let s1 = String::from("hello");
let s2 = s1; // s1 is MOVED to s2
// println!("{}", s1); // ERROR: s1 is no longer valid
println!("{}", s2); // Only s2 is valid
}
Move vs Copy
Some types are duplicated on assignment instead of moved: the ones that implement the Copy trait. Copy is a trait a type opts into, not something the compiler infers from how big the value is — a type may implement it only if every one of its fields is itself Copy and the type has no Drop impl. That is why every integer, float, bool, char, and tuples of those are Copy, while String and Vec<T> are not — they own a heap buffer that has to be freed exactly once, and a second copy of the same pointer would free it twice. Size is a consequence, not the cause: String has a perfectly well-known compile-time size (24 bytes on a 64-bit target) and still moves, and &mut i32 is one pointer wide and still moves, while &i32 is the same width and copies. Compare the two assignments below:
fn main() {
// These types implement Copy
let a: i32 = 5;
let b = a; // copy
println!("a = {}, b = {}", a, b); // Both valid
// Strings do NOT implement Copy
let s1 = String::from("hello");
let s2 = s1; // move, not copy
// s1 is no longer valid here
}
Those two let b = a; lines look identical, and that is exactly the trap. Whether the original name survives the assignment depends entirely on the type on the right-hand side, not on how the line is written. Below, line D is commented out because leaving it in would stop the whole program from building. Work out why it is the one that had to go before you run this.
Predict
Lines A and B look like the same operation, but only one of them leaves the original variable usable — which is why line D had to be commented out while line C compiles. What is the difference, and what does this program print?
fn main() {
let count = 3;
let label = String::from("boxes");
let count_again = count; // line A
let label_again = label; // line B
// Exactly ONE of these two lines compiles. The other is commented out
// because leaving it in would stop the whole program from building.
println!("{} {}", count, count_again); // line C
// println!("{} {}", label, label_again); // line D
println!("{}", label_again);
}The program prints 3 3 and then boxes, and the line that had to be commented out is D, not B. i32 implements Copy, so line A duplicates the value and both count and count_again stay valid — line C prints 3 3. String owns a heap buffer, so it does not implement Copy: line B moves ownership into label_again, and from that point label is dead, though label_again prints boxes quite happily. Moving is always allowed; it is the later use that fails, which is why the error would land on line D. Keep that split in mind — the line the compiler blames is almost never the line that caused the problem.
When the Owner Goes Away
Rule three says the value is dropped when its owner goes out of scope. "Out of scope" covers more cases than the end of a function: a bare { } block is a scope, and handing a value to a function that takes it by value makes that function's parameter the new owner — so the value dies when the callee returns, not when main does. A type can observe this by implementing Drop.
struct Noisy {
name: String,
}
impl Drop for Noisy {
fn drop(&mut self) {
println!("dropping {}", self.name);
}
}
fn consume(item: Noisy) {
println!("consume got {}", item.name);
} // item is the owner now, so it is dropped HERE
fn main() {
let outer = Noisy { name: String::from("outer") };
println!("main owns {}", outer.name);
{
let inner = Noisy { name: String::from("inner") };
println!("block owns {}", inner.name);
} // inner goes out of scope here
let handed_off = Noisy { name: String::from("handed-off") };
consume(handed_off); // ownership moves into consume
println!("end of main");
} // outer is dropped last
Running that prints, in order: main owns outer, block owns inner, dropping inner, consume got handed-off, dropping handed-off, end of main, dropping outer. Three things to take from the trace. The inner block's value dies at the closing brace, long before main ends. The value handed to consume dies inside consume, because passing by value transfers ownership — main never gets it back. And outer, declared first, is dropped last, after end of main prints.
Borrowing with References
Instead of taking ownership, you can borrow a value using references:
fn main() {
let s1 = String::from("hello");
// &s1 creates a reference to s1
let len = calculate_length(&s1);
// s1 is still valid because we only borrowed it!
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
} // s goes out of scope, but it doesn't own the String
Forgetting that & is the single most common way a Rust beginner's program fails to compile. The program below hands a String to a helper by value and then tries to use it afterwards — the exact use-after-move the Predict above described, but this time you have to spot it in real code and read the compiler's own report. Commit a hypothesis about which line the compiler will blame before you touch anything.
Debug
This program prints a receipt label inside a box, then reports on it. It refuses to compile. The compiler names one variable and two different lines — work out which line actually caused the problem, then fix it so the program prints its box and then 'All checks passed.'
fn main() {
let receipt = String::from("order-4417");
// Hand the receipt to a helper that prints it in a box.
let width = print_boxed(receipt);
// Then report on the same receipt afterwards.
println!("printed {} in a box {} chars wide", receipt, width);
assert_eq!(width, 14, "the box should be the label plus 4 characters");
println!("All checks passed.");
}
fn print_boxed(label: String) -> usize {
let width = label.len() + 4;
println!("+{}+", "-".repeat(width - 2));
println!("| {} |", label);
println!("+{}+", "-".repeat(width - 2));
width
}Expected output: +------------+
| order-4417 |
+------------+
printed order-4417 in a box 14 chars wide
All checks passed.
The parameter type is the bug: print_boxed(label: String) takes the receipt by value, so calling it moves receipt into the function, and the String is dropped when print_boxed returns. By the time the println! runs, main has nothing left to print — hence error[E0382]: borrow of moved value: receipt. Note the compiler names two lines: the call is where the move happened, the println! is where it was noticed. The fix is one ampersand in each place: fn print_boxed(label: &String) and print_boxed(&receipt). The .clone() that rustc also suggests works too, but it copies the whole buffer to dodge a single character — borrow first, clone only when you truly need a second independent value.
Recall
Without scrolling up: back in Variables and Data Types you learned that Rust variables are immutable unless you write mut. Ownership is a different axis entirely. Given let s = String::from('hi'); — with no mut — which of these is true?
To say it plainly: mut and ownership are independent. mut decides whether you may change a value through a given name; ownership decides who is responsible for the value and when it is dropped. An immutable binding can still be moved away, and once it is, the problem is not immutability but that the name no longer owns anything. The next section combines both ideas — a mutable reference lends a value and permits changing it.
Mutable References
By default, references are immutable. To modify borrowed data, use mutable references:
fn main() {
let mut s = String::from("hello");
change(&mut s);
println!("{}", s); // Prints "hello, world"
}
fn change(s: &mut String) {
s.push_str(", world");
}
The Rules of References
- You can have either one mutable reference or any number of immutable references
- References must always be valid (no dangling references)
fn main() {
let mut s = String::from("hello");
// Multiple immutable references are OK
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
// After r1 and r2 are no longer used, we can have a mutable reference
let r3 = &mut s;
r3.push_str(" world");
println!("{}", r3);
}
Preventing Data Races
This restriction prevents data races at compile time:
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
// let r2 = &mut s; // ERROR: cannot have two mutable references
println!("{}", r1);
}
The Slice Type
Slices let you reference a contiguous sequence of elements rather than the whole collection:
fn main() {
let s = String::from("hello world");
let hello = &s[0..5]; // or &s[..5]
let world = &s[6..11]; // or &s[6..]
println!("{} {}", hello, world);
}
Practice Exercise
Reading about borrowing is not the same as choosing the right signature under your own name. This is a build task: a small program that reports its own pass/fail. One String lives in main and never leaves it; three helper functions have to work on it without taking it away. Run the starter as-is and it fails immediately, naming the first check that did not pass. Implement each function until every check passes and it prints All checks passed.
The three signatures are already written for you, and they are the lesson in miniature: two take &String (lend, read only) and one takes &mut String (lend, and allow changes). Watch the second check in particular — it asserts that main still owns its String after the first call, which is the property a by-value parameter would silently destroy.
Build
Finish the build. Three functions are stubbed out and the checks below them fail until each one behaves. Run it as-is to see which check fails first, decide what that function is missing, then implement all three until it prints 'All checks passed.' The checks run top to bottom, so the first failure you see is TODO 1 — implement it first, then work down. Do not change any of the three signatures: the whole point is that none of them takes ownership.
// TODO 1: count the words in text WITHOUT taking ownership of it.
// The caller must still own its String afterwards, so this borrows.
// word_count(&String::from("a b c")) -> 3
// Hint: split_whitespace() gives an iterator; count() consumes it.
fn word_count(text: &String) -> usize {
let _ = text;
0
}
// TODO 2: return a NEW uppercased String, leaving text untouched.
// shout(&String::from("hi")) -> "HI", and the caller's "hi" is unchanged.
fn shout(text: &String) -> String {
let _ = text;
String::new()
}
// TODO 3: append note to text IN PLACE through the mutable borrow.
// append_note(&mut s, "!") leaves the caller's s one character longer.
fn append_note(text: &mut String, note: &str) {
let _ = text;
let _ = note;
}
// --- Build checks: these must all pass. Do not edit below this line. ---
fn main() {
let mut report = String::from("all systems nominal");
assert_eq!(word_count(&report), 3, "word_count should see 3 words");
assert_eq!(report.len(), 19, "word_count must BORROW: report is still owned by main");
let loud = shout(&report);
assert_eq!(loud, "ALL SYSTEMS NOMINAL", "shout should return an uppercased copy");
assert_eq!(report, "all systems nominal", "shout must not disturb the original");
append_note(&mut report, " (checked)");
assert_eq!(report, "all systems nominal (checked)", "append_note should extend report in place");
assert_eq!(word_count(&report), 4, "the appended note adds a fourth word");
println!("All checks passed.");
println!("Report: {}", report);
println!("Shouted: {}", loud);
println!("Words: {}", word_count(&report));
}Expected output: All checks passed.
Report: all systems nominal (checked)
Shouted: ALL SYSTEMS NOMINAL
Words: 4
Once it passes, try two variations and predict each before running:
- Take ownership instead of borrowing. Change
word_count's signature tofn word_count(text: String)and drop the&from all three of its call sites. Predict which check breaks before running. It does not fail a check at all — it never gets that far. Compilation fails witherror[E0382]: borrow of moved value: report, pointing at the very next line:report.len()reads aStringthat was moved intoword_countand dropped there. (Change only the first call site and you geterror[E0308]: mismatched typesat the other two instead — a signature and its callers have to agree before ownership is even considered.) Exactly the bug from the debug block above, reintroduced by a missing ampersand. - Drop the
muton the borrow. Changeappend_note's parameter totext: &String(leavingpush_strin the body). Predict what the compiler says before running. You geterror[E0596]: cannot borrow *text as mutable, as it is behind a & reference—push_strrequires a mutable borrow, so the signature and the operation have to agree. This is the compiler enforcing the read-only promise the&made.
Key Takeaways
- Ownership ensures memory safety without a garbage collector
- Each value has exactly one owner
- When the owner goes out of scope, the value is dropped
- References allow borrowing without taking ownership
- You can have one mutable reference OR multiple immutable references
- The borrow checker enforces these rules at compile time
Understanding ownership is fundamental to writing safe, efficient Rust code!
Next Steps
Now that you understand ownership, you're ready to learn about structs and enums — Rust's tools for building custom data types that work hand in hand with the ownership system. You'll see how they let you model your domain precisely while the borrow checker keeps the data safe.
Next lesson
Structs & Enums
Learn how to define Rust structs and enums to create custom data types, implement methods, and model your domain effectively
20 min