What Is Ownership in Rust?
Ownership is Rust’s most famous feature — and the one that scares most beginners. It shouldn’t. The core idea fits in one sentence:
Every value in Rust has exactly one owner, and when that owner goes away, the value is cleaned up.
Think of a value as a physical book. Only one person can hold the book at a time. You can hand it over to a friend (then it’s theirs, not yours), you can photocopy it (now there are two books), and when the last holder leaves the room, the book gets returned. That’s ownership. Keep this picture in mind — we’ll use it through the whole article.
Ownership is how Rust answers a hard question every language must answer: who frees memory, and when?
| Approach | How it works | Used by | Cost |
|---|---|---|---|
| Garbage collector | A background process finds unused memory and frees it | Python, Java, JavaScript, Go | Slower, unpredictable pauses |
| Manual management | You call allocate and free yourself | C, C++ | Easy to get wrong — leaks, crashes |
| Ownership | The compiler figures out where to free memory using rules it checks at compile time | Rust | Zero runtime cost — but you must learn the rules |
Rust gets memory safety without a garbage collector and without you ever calling free. If you break an ownership rule, your program simply doesn’t compile.
Don’t worry if ownership doesn’t fully “click” today. That’s normal — everyone needs a few rounds with the compiler before it becomes second nature. The compiler’s error messages are genuinely helpful, and they will do a lot of the teaching.
First, a Quick Detour: The Stack and the Heap
To understand ownership you need to know where your program keeps data while it runs. There are two places: the stack and the heap.
The stack is like a stack of plates. You add new plates on top and take plates off the top — never from the middle. In programming terms this is last in, first out (LIFO). Adding data is called pushing, removing it is called popping. The stack is extremely fast, but there’s a catch: everything stored on it must have a known, fixed size at compile time.
The heap is like getting a table at a restaurant. You tell the host “table for four, please.” The host looks around, finds a free table that fits, and tells you where it is. That “where it is” — the address — is called a pointer. The heap can hold data of any size, including data that grows and shrinks while the program runs. The price: it’s slower, because the allocator has to find space, and you always reach the data by following a pointer.
| Stack | Heap | |
|---|---|---|
| Speed | Very fast | Slower |
| Size of data | Must be known and fixed | Can be unknown or grow |
| Access | Direct | Through a pointer |
| Cleanup | Automatic — popped off | Someone must free it |
That last row is the whole point. Stack data cleans itself up when a function ends. Heap data doesn’t — someone has to free it. Tracking who frees heap memory, and making sure it happens exactly once, is the problem that ownership solves.
The Three Rules of Ownership
These three rules are the entire foundation of this chapter. Everything else is just these rules playing out.
- Each value in Rust has an owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value is dropped.
In our book analogy: every book is held by someone (rule 1), only one person holds it at a time (rule 2), and when the holder leaves the room, the book is returned (rule 3).
Variable Scope
A scope is the region of code where a variable is valid — usually the area between { and } braces. This part works like most other languages:
fn main() {
{ // s does not exist yet
let s = "hello"; // s is valid from this point forward
println!("{s}"); // we can use s here
} // the scope is over — s is no longer valid
// println!("{s}"); // ❌ this would not compile
}The variable s is valid from the moment it’s declared until the end of its scope. Simple. But now let’s look at data that lives on the heap — that’s where ownership starts doing real work.
The String Type
The strings we’ve used so far were string literals like "hello" — hardcoded into the program, immutable, with a size known at compile time. They live happily on the stack.
But what if you need text that can change or whose size you don’t know in advance — like something the user types in? For that, Rust has the String type, which stores its text on the heap:
fn main() {
let mut s = String::from("hello");
s.push_str(", world!"); // push_str() appends text onto the String
println!("{s}");
}cargo runOutput:
Compiling rust_app v0.1.0 (/home/username/desktop-app/rust_app)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.19s
Running `target/debug/rust_app`
hello, world!A literal can’t do this — String can grow because its text lives on the heap, where there’s room to grow. (If the mut keyword is new to you, we covered it in Variables and Mutability.)
What a String Looks Like in Memory
Here’s the picture that makes the rest of this chapter easy. A String is actually two pieces:
let s1 = String::from("hello"); — the bookkeeping lives on the stack, the actual text lives on the heap.
On the stack, s1 is just three small, fixed-size fields:
- ptr — a pointer to where the text lives on the heap
- len — how many bytes the text currently uses (5 for “hello”)
- capacity — how many bytes were reserved on the heap
The actual text — the bytes h, e, l, l, o — lives on the heap.
When does that heap memory get allocated and freed?
- Allocated when you call
String::from("hello")— that’s the “table for five, please” moment. - Freed when the owner goes out of scope. At the closing
}, Rust automatically calls a special function nameddropthat returns the memory. You never call it yourself.
No garbage collector scanning in the background, no manual free() call to forget. The compiler simply inserts the cleanup at the exact point the owner’s scope ends. This pattern is famous in C++ as RAII — but in Rust it’s not a convention, it’s the language.
Move: What Happens When You Assign a String
Here’s where Rust surprises everyone coming from other languages. What do you think this prints?
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{s1}, world!");
}In Python or JavaScript, this would print hello, world! without complaint. In Rust, it doesn’t even compile:
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:5:15
|
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 |
5 | println!("{s1}, world!");
| ^^^^ value borrowed here after move
|
help: consider cloning the value if the performance cost is acceptable
|
3 | let s2 = s1.clone();
| ++++++++
For more information about this error, try `rustc --explain E0382`.This is error[E0382]: borrow of moved value — the first error nearly every new Rust programmer meets, and one of the most googled Rust errors of all. The compiler says s1 was moved. Let’s see what that actually means (and by the end of this section, how to fix it).
Why Rust Can’t Just Copy
When you write let s2 = s1;, Rust copies the stack part — the pointer, length, and capacity. It does not copy the heap data. Copying three small fields is cheap; copying the heap data could be gigabytes. So after the assignment, memory looks like this:
Only the stack part (ptr, len, capacity) is copied — the heap data is not.
See the problem? Two variables point at the same heap data. Remember rule 3: when a variable goes out of scope, Rust frees its heap data. If s1 and s2 were both valid, both would try to free the same memory — once for s1, once for s2. That’s called a double free, a classic C/C++ bug that corrupts memory and creates security holes.
But rule 2 says there can only be one owner at a time. So Rust does something elegant:
What Rust actually does: the value has moved — s1 stops being valid, and s2 is the one and only owner.
The moment you write let s2 = s1;, Rust considers s1 no longer valid. The value hasn’t been copied — it has moved from s1 into s2. You handed the book to a friend. It’s their book now; you can’t read it anymore.
And now the cleanup question has a clean answer: only s2 frees the memory. A double free is impossible — not “unlikely if you’re careful,” but impossible, checked by the compiler.
Moves are how Rust stays fast by default. Rust will never deep-copy your heap data behind your back. Any copy that happens automatically is guaranteed to be cheap (just the small stack part). Expensive copies only happen when you explicitly ask for them — which is next.
Assigning a New Value Drops the Old One Immediately
One more nice consequence. If you overwrite a String, Rust drops the old heap data right there — it doesn’t wait for the end of the scope:
fn main() {
let mut s = String::from("hello");
s = String::from("ahoy"); // "hello" has no owner now → freed immediately
println!("{s}, world!");
}Output:
ahoy, world!Nothing points to "hello" anymore, so its memory is returned on the spot. No leak.
Clone: When You Really Do Want a Copy
Sometimes you genuinely want two independent copies of the data — you want to photocopy the book, not hand it over. That’s what .clone() is for:
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // deep copy: heap data is duplicated
println!("s1 = {s1}, s2 = {s2}"); // ✅ both are valid
}Output:
s1 = hello, s2 = helloCopying heap data costs real work, so Rust makes you ask for it explicitly with .clone().
Now there are two allocations on the heap, each with its own owner. Both variables are valid, both will be freed independently. Everyone’s happy.
.clone() can be expensive for large data — and that’s exactly why Rust makes you write it out. When you see .clone() in code, you know real work is happening. Nothing costly ever hides in an innocent-looking =.
Copy: Why Integers Don’t Play by These Rules
Hold on, though. This code looks just like the String example that failed — but it compiles fine:
fn main() {
let x = 5;
let y = x;
println!("x = {x}, y = {y}"); // ✅ both still valid!
}Output:
x = 5, y = 5Why is x still usable when s1 wasn’t? Because an integer has no heap part:
Both x and y stay valid — copying 4 bytes on the stack is basically free.
An i32 is just 4 bytes sitting directly on the stack. Copying it is a single, trivial operation — there’s no pointer, no shared heap data, no double-free danger. A “shallow” copy and a “deep” copy would be the exact same thing. So Rust just copies it, and both variables stay valid.
In the book analogy: an integer isn’t a book, it’s a fact — like “the meeting is at 5.” Telling a friend doesn’t remove it from your head. Now you both know it.
Types that behave this way implement the Copy trait. The common ones (you met them all in Data Types):
- All integer types —
i32,u64, and friends - The boolean type —
bool - All floating-point types —
f32,f64 - The character type —
char - Tuples, but only if every field is
Copy—(i32, i32)isCopy;(i32, String)is not
A type can implement Copy only if it needs no cleanup when it goes out of scope. Copy and Drop are mutually exclusive — a type that owns heap data (like String) can never be Copy.
Ownership and Functions
Here’s the part that trips people up in real code: passing a value to a function works exactly like assignment. A String moves in; an i32 is copied in. (Need a refresher on how functions work in Rust? See Functions.)
fn main() {
let s = String::from("hello");
takes_ownership(s); // s MOVES into the function...
// ...s is no longer valid here!
let x = 5;
makes_copy(x); // x is COPIED into the function...
println!("x is still valid here: {x}"); // ...so x still works ✅
}
fn takes_ownership(some_string: String) {
println!("{some_string}");
} // some_string goes out of scope here → `drop` is called, memory freed
fn makes_copy(some_integer: i32) {
println!("{some_integer}");
} // some_integer goes out of scope → just popped off the stack, nothing specialOutput:
hello
5
x is still valid here: 5A String moves into the function and is dropped when the function’s scope ends. An i32 is copied, so the original keeps working.
If you try to use s after the call, the compiler stops you — and look how precisely it explains the problem:
fn main() {
let s = String::from("hello");
takes_ownership(s);
println!("{s}"); // ❌ s was moved into the function
}error[E0382]: borrow of moved value: `s`
--> src/main.rs:5:15
|
2 | let s = String::from("hello");
| - move occurs because `s` has type `String`, which does not implement the `Copy` trait
3 | takes_ownership(s);
| - value moved here
4 |
5 | println!("{s}");
| ^^^ value borrowed here after move
|
note: consider changing this parameter type in function `takes_ownership` to borrow instead
if owning the value isn't necessaryYou handed the book into the function. The function finished, its scope ended — and the book was cleaned up right there.
Return Values Give Ownership Back
Functions can also hand ownership out through their return value:
fn main() {
let s1 = gives_ownership(); // return value moves INTO s1
let s2 = String::from("hello");
let s3 = takes_and_gives_back(s2); // s2 moves in, result moves out to s3
// (s2 is no longer valid)
println!("s1 = {s1}, s3 = {s3}");
}
fn gives_ownership() -> String {
let some_string = String::from("yours");
some_string // returned → moves out to whoever called us
}
fn takes_and_gives_back(a_string: String) -> String {
a_string // comes in, goes right back out
}Output:
s1 = yours, s3 = helloThe pattern is always the same: assigning, passing, and returning all move ownership (unless the type is Copy). And when a variable holding heap data goes out of scope still owning its value, that value is dropped.
But Isn’t This… Tedious?
Yes! Suppose you just want a function to read a String — say, measure its length — without taking it away from you. With only the tools from this chapter, you’d have to pass ownership in and ship it back out in a tuple:
fn main() {
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1); // give it away, get it back...
println!("The length of '{s2}' is {len}.");
}
fn calculate_length(s: String) -> (String, usize) {
let length = s.len();
(s, length) // ...along with the answer we actually wanted
}Output:
The length of 'hello' is 5.It works, but it’s a lot of ceremony for “how long is this string?” You shouldn’t have to give away your book just so someone can count its pages. They should be able to borrow it, look, and hand it straight back.
Rust agrees — that feature is called references and borrowing, and it’s the subject of the next chapter.
Summary
| You write | What happens | Original still valid? |
|---|---|---|
let s2 = s1; (a String) | Move — only stack fields copied, s1 invalidated | ❌ No |
let s2 = s1.clone(); | Deep copy — heap data duplicated | ✅ Yes |
let y = x; (an i32) | Copy — trivial stack copy | ✅ Yes |
some_fn(s) (a String) | Move into the function, dropped when it ends | ❌ No |
some_fn(x) (an i32) | Copy into the function | ✅ Yes |
return s; | Move out to the caller | — |
And the three rules once more — they run this whole show:
- Each value has an owner.
- Only one owner at a time.
- Owner goes out of scope → value is dropped.
Frequently Asked Questions
What are the three rules of ownership in Rust?
Each value in Rust has an owner; there can only be one owner at a time; and when the owner goes out of scope, the value is dropped and its memory is freed. The compiler enforces all three rules at compile time — breaking one is a compile error, never a runtime crash.
What is the difference between move, clone, and copy in Rust?
Assigning a heap type like String moves ownership — only the stack fields are copied, and the original variable becomes invalid. Calling .clone() makes a deep copy of the heap data, so both variables stay valid, but it costs real work. Stack-only types like i32, bool, char, and f64 implement the Copy trait and are duplicated automatically, because copying a few bytes on the stack is trivial.
How do I fix “error[E0382]: borrow of moved value”?
The error means you used a variable after its value was moved. You have three options: reorder your code so you use the value before the move, call .clone() if you genuinely need two independent copies, or — the usual best fix — pass a reference (&T) so the function borrows the value instead of taking ownership. Borrowing is covered in the next chapter.
Why doesn’t Rust have a garbage collector?
It doesn’t need one. The ownership rules let the compiler know, at compile time, exactly when each value stops being used — so it inserts the cleanup (a call to drop) automatically at the right spot. Memory is freed deterministically, with zero runtime overhead and no GC pauses.
Does ownership make Rust slower?
No. Ownership is checked entirely at compile time, so there is no runtime cost at all. Moves themselves are cheap — Rust copies only a few small stack fields (pointer, length, capacity) and never deep-copies heap data unless you explicitly ask with .clone().
What’s Next?
Ownership answers who frees the memory. But constantly handing values back and forth is clumsy — what you usually want is to let a function borrow a value temporarily, without ownership changing hands at all.
That’s exactly what references and borrowing do, and they’re where Rust’s design really starts to shine. See you in the next chapter!
Until then, you can revisit Control Flow or browse the rest of the Rust tutorial.