Chapter 03
Ownership - The Big Idea
The concept that makes Rust different from every other mainstream language. Master this and the rest of Rust falls into place.
What Ownership Is and Why Rust Needs It
Every program has to manage memory. In C, you manually malloc and free. In Java, Python, or Go, a garbage collector runs in the background, scanning for memory that's no longer used and freeing it. Both approaches have real costs: manual management leads to use-after-free bugs and memory leaks; garbage collection adds latency and unpredictable pauses.
Rust takes a third path: ownership. The compiler tracks who owns each piece of data, and it inserts the deallocation code at compile time - exactly where it's needed. No manual frees, no GC pauses. If your code compiles, the memory management is correct.
The rules are simple. Every value in Rust has:
- Exactly one owner at any given time.
- When the owner goes out of scope, the value is dropped (deallocated).
- Ownership can be transferred (moved) to another variable.
That's it. These three rules, enforced at compile time, give Rust memory safety without runtime overhead. The rest of this chapter is about what those rules mean in practice.
The Stack vs the Heap
To understand ownership, you need to understand where data lives. Rust uses two regions of memory:
The Stack
- Fixed-size data: integers, bools, chars, floats
- Extremely fast - just move a pointer
- Data is automatically cleaned up when the function returns
- Size must be known at compile time
The Heap
- Dynamic-size data:
String,Vec, anything growable - Slower - requires asking the OS for memory
- Must be explicitly freed (Rust does this for you via ownership)
- Size can change at runtime
When you write let port: u16 = 8080;, that 16-bit integer lives on the stack. When you write let body = String::from("Hello");, the String struct itself is on the stack (a pointer, a length, and a capacity - 24 bytes on a 64-bit system), but the actual character data "Hello" is on the heap.
Stack Heap
┌──────────────────┐ ┌───────────────┐
│ body │ │ │
│ ptr ──────────────▶ │ H e l l o │
│ len: 5 │ │ │
│ cap: 5 │ └───────────────┘
└──────────────────┘This matters because ownership is primarily about heap data. Stack data is cheap to copy - the compiler handles it silently. Heap data is expensive, so Rust makes you think about who owns it.
Move Semantics and Copy Types
Here's where ownership gets concrete. When you assign a heap value to another variable, the ownership moves:
let response = String::from("HTTP/1.1 200 OK");
let saved = response;
// This will NOT compile:
println!("{}", response); // ERROR: value used after moveAfter let saved = response;, the variable response is no longer valid. Rust moved the ownership to saved. The data itself didn't move in memory - the pointer, length, and capacity were copied on the stack, and the old variable was invalidated. This prevents two variables from pointing to the same heap data, which would lead to a double-free when both go out of scope.
Before move:
response ──▶ "HTTP/1.1 200 OK"
After move:
response (invalid)
saved ──▶ "HTTP/1.1 200 OK"The same thing happens when you pass a value to a function:
fn log_response(resp: String) {
println!("Sending: {}", resp);
} // resp is dropped here - memory freed
let response = String::from("HTTP/1.1 200 OK");
log_response(response);
// Can't use response anymore - it was moved into the function
println!("{}", response); // ERROR: value used after moveBut stack-only types like integers, floats, and booleans behave differently. They implement the Copy trait, which means assignment copies the value instead of moving it:
let port: u16 = 8080;
let backup = port; // copies the value - both are valid
println!("port: {}, backup: {}", port, backup); // fine!This makes sense: copying 2 bytes on the stack is essentially free. There's no heap allocation to worry about. Types that implement Copy include all integer types, f32, f64, bool, char, and tuples of Copy types.
String does not implement Copy. If you need a duplicate, you must be explicit:
let response = String::from("HTTP/1.1 200 OK");
let saved = response.clone(); // explicit deep copy
println!("response: {}", response); // fine - response still valid
println!("saved: {}", saved); // fine - independent copy.clone() copies the heap data. For a short string, it's fine. For a large HTTP response body, it's a real cost. Rust makes this explicit so you're always aware of when you're doing expensive work.References and Borrowing
Moving ownership everywhere would be impractical. Most of the time, a function just needs to look at data, not own it. That's what references are for. A reference lets you access data without taking ownership - this is called borrowing.
fn content_length(body: &str) -> usize {
body.len()
}
let body = String::from("<h1>Hello</h1>");
let len = content_length(&body); // borrow body - don't move it
// body is still valid here - we only lent it out
println!("Body ({} bytes): {}", len, body);The & creates a reference. The function receives a &str - a borrowed view of the string data. When the function returns, the borrow ends. The caller still owns the data.
Think of it like lending someone a book. They can read it, but you still own it and get it back when they're done.
body ──────▶ "<h1>Hello</h1>"
▲
&body ────────────┘ (points to the same data, doesn't own it)Mutable References
By default, references are immutable - you can look but not touch. If a function needs to modify borrowed data, you use a mutable reference:
fn add_header(response: &mut String, name: &str, value: &str) {
response.push_str("\r\n");
response.push_str(name);
response.push_str(": ");
response.push_str(value);
}
let mut response = String::from("HTTP/1.1 200 OK");
add_header(&mut response, "Content-Type", "text/html");
add_header(&mut response, "Server", "rust-webserver");
println!("{}", response);
// HTTP/1.1 200 OK
// Content-Type: text/html
// Server: rust-webserverNotice the chain: the variable must be declared mut, and the reference must be &mut. Both the owner and the borrower must agree that mutation is allowed.
The Borrowing Rules
Rust enforces two rules about references at compile time. These prevent entire categories of bugs:
- You can have many immutable references (
&T) at the same time - multiple readers are fine. - You can have one mutable reference (
&mut T) at a time - and while it exists, no immutable references are allowed either.
In short: either many readers, or one writer. Never both.
let mut data = String::from("hello");
let r1 = &data; // fine
let r2 = &data; // fine - multiple immutable borrows OK
println!("{} {}", r1, r2);
let w = &mut data; // fine - r1 and r2 are no longer used
w.push_str(" world");let mut data = String::from("hello");
let r1 = &data;
let w = &mut data; // ERROR: cannot borrow as mutable
// because it's also borrowed as immutable
println!("{}", r1); // r1 is still alive here - conflictThe Borrow Checker: Reading Its Errors
The borrow checker is the part of the compiler that enforces ownership and borrowing rules. When you violate them, you get an error. Rust's error messages are designed to be helpful - let's learn how to read them.
Consider this code:
fn main() {
let response = String::from("OK");
let moved = response;
println!("{}", response);
}The compiler produces:
error[E0382]: borrow of moved value: `response`
--> src/main.rs:4:20
|
2 | let response = String::from("OK");
| -------- move occurs because `response` has type `String`
3 | let moved = response;
| -------- value moved here
4 | println!("{}", response);
| ^^^^^^^^ value borrowed here after move
|
= note: consider cloning the value if you need it in both placesEvery borrow checker error tells you:
- What went wrong - "borrow of moved value"
- Where it happened - with line numbers and arrows pointing at the exact expressions
- Why - "move occurs because response has type String" (heap type, so it moves instead of copying)
- How to fix it - "consider cloning the value"
The fix depends on what you actually need. Common strategies:
- Borrow instead of move: pass
&responseinstead ofresponse - Clone: use
response.clone()when you need an independent copy - Restructure: reorder code so the original is used before the move
.clone() everywhere. First ask: "does this function really need to own the data, or can it just borrow?" Most of the time, a reference is the right answer.Here are the error codes you'll see most often as a beginner:
E0382 - use of moved value
You tried to use a variable after its ownership was transferred. Fix: borrow, clone, or restructure.
E0502 - immutable and mutable borrow conflict
You have an & and &mut alive at the same time. Fix: finish using the immutable borrow before creating the mutable one.
E0499 - two mutable borrows at once
Only one &mut is allowed at a time. Fix: limit the scope of the first borrow so it ends before the second begins.
E0106 - missing lifetime specifier
The compiler needs to know how long a reference lives. We'll cover this below and in depth in Chapter 16.
Lifetimes: A First Look
Every reference in Rust has a lifetime - the scope during which the reference is valid. Most of the time, the compiler figures this out on its own. But sometimes, especially when a function returns a reference, you need to help.
Consider this function that tries to return a reference to the longer of two strings:
// This won't compile - the compiler doesn't know
// which input the return value refers to
fn longest(a: &str, b: &str) -> &str {
if a.len() >= b.len() { a } else { b }
}The problem: the return value is a reference, but to what? If the caller passes two strings with different lifetimes, which lifetime does the return value get? The compiler can't guess.
You tell it using lifetime annotations:
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}The 'a (pronounced "tick a") is a lifetime parameter. This signature says: "the returned reference will live at least as long as both inputs." The compiler uses this to verify the caller isn't using the result after either input is dropped.
For now, just remember the key idea: a reference must never outlive the data it points to. This is how Rust prevents dangling pointers - at compile time, not runtime.
// This will NOT compile:
fn dangling() -> &String {
let s = String::from("hello");
&s // ERROR: s is dropped at end of function,
// but we're trying to return a reference to it
}
// Fix: return the owned String instead
fn not_dangling() -> String {
let s = String::from("hello");
s // ownership moves to the caller - no dangling reference
}Putting It Together
Let's update our webserver code to practice ownership and borrowing. We'll write functions that build HTTP responses, taking care to use references where functions just need to read data and owned types where functions need to produce new data:
/// Build an HTTP response as a single String.
/// Takes borrowed inputs - it reads them but doesn't need to own them.
fn build_response(status: u16, reason: &str, body: &str) -> String {
let status_line = format!("HTTP/1.1 {} {}", status, reason);
let content_length = body.len();
format!(
"{}\r\nContent-Length: {}\r\n\r\n{}",
status_line, content_length, body
)
}
/// Append a header to an existing response string.
/// Takes a mutable reference - it modifies the caller's data in place.
fn add_header(response: &mut String, name: &str, value: &str) {
// Find the end of headers (before the blank line)
if let Some(pos) = response.find("\r\n\r\n") {
let header = format!("\r\n{}: {}", name, value);
response.insert_str(pos, &header);
}
}
fn main() {
let body = "<h1>Hello from Rust!</h1>";
// build_response borrows body - we can still use it after
let mut response = build_response(200, "OK", body);
// add_header borrows response mutably - modifies it in place
add_header(&mut response, "Content-Type", "text/html");
add_header(&mut response, "Server", "rust-webserver/0.1");
println!("{}", response);
println!("---");
// body is still valid - it was only borrowed, not moved
println!("Original body: {}", body);
println!("Body length: {}", body.len());
}Run it with cargo run. Notice how build_response borrows its inputs with &str and returns an owned String, while add_header takes &mut String to modify the response in place. This is a pattern you'll use constantly: borrow for input, own for output, mutably borrow for in-place modification.
Exercise
- Write a function
fn first_line(text: &str) -> &strthat returns everything before the first\n. Think about why the return type can be&str- where does the data live? - Deliberately create a move error: assign a
Stringto a new variable, then try to use the original. Read the full error message. Fix it three ways: with a reference, with.clone(), and by reordering the code. - Try to create a mutable borrow conflict: take
&mutto aString, then try to read it with&while the mutable borrow is still alive. Read the error, then fix it by limiting the mutable borrow's scope with a block{ }. - Modify
build_responseto accept an ownedStringfor the body instead of&str. What changes inmain? Why is the borrowed version more flexible?
What's Next
You now understand the foundation that everything in Rust is built on. Ownership, moves, borrowing, and lifetimes aren't just academic concepts - they're how Rust prevents memory bugs, data races, and dangling pointers at compile time.
In the next chapter, we'll learn structs and enums - the tools for modeling data. We'll define types to represent HTTP requests and responses, and use pattern matching to handle different cases cleanly. Ownership rules will apply to every struct field and every enum variant.