Ownership
Reviewed & published by Brayan K
Rust is a systems programming language focused on speed, memory safety, and fearless concurrency — and ownership is the single feature that makes all of that possible without a garbage collector.
Part of the free Rust course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll learn the three rules of ownership, what it means for a value to be moved, why some types copy instead, and how ownership flows through functions.
What You'll Learn in This Lesson
- The three rules of ownership
- What happens when a value is moved
- Why String moves but i32 copies
- How .clone() makes a deep copy
- How ownership passes into and out of functions
- When values are dropped and memory is freed
1️⃣ Moves, Clones, and Copy Types
Every value has exactly one owner. For a heap type like String, assigning it to another variable moves ownership, and the original becomes invalid — this stops two variables from freeing the same memory. If you need two independent copies, call .clone(). Small stack types like i32 implement Copy, so they are duplicated cheaply and both stay valid.
fn main() {
// A String stores its text on the HEAP. It has ONE owner at a time.
let s1 = String::from("hello");
// Assigning s1 to s2 MOVES ownership. s1 is no longer valid afterwards.
let s2 = s1;
// Trying to use s1 now would be a compile error:
// println!("{}", s1); // error[E0382]: borrow of moved value: s1
println!("s2 owns: {}", s2); // -> s2 owns: hello
// To keep both, .clone() makes a deep copy on the heap.
let s3 = s2.clone();
println!("s2 = {}, s3 = {}", s2, s3); // both valid now
// Simple stack types like i32 implement Copy, so they are COPIED, not moved.
let x = 5;
let y = x; // x is still usable
println!("x = {}, y = {}", x, y); // -> x = 5, y = 5
}The crucial line is let s2 = s1; — after it, s1 is moved-from and unusable. Contrast that with let y = x; for the integer, where both remain valid because i32 is Copy.
2️⃣ Ownership and Functions
Passing a value to a function follows the same rules. A String argument is moved into the function; afterward the original variable is gone. A Copy type is copied in, leaving the original usable. A function can also return a value, moving ownership back out to the caller.
fn main() {
let name = String::from("Ada");
// Passing a String to a function MOVES it in — name is gone afterwards.
takes_ownership(name);
// println!("{}", name); // error: name was moved into the function
// A Copy type like i32 is copied in, so the original stays usable.
let n = 10;
makes_copy(n);
println!("n is still {}", n); // -> n is still 10
// A function can GIVE ownership back by returning a value.
let made = gives_ownership();
println!("got: {}", made); // -> got: a fresh string
}
fn takes_ownership(text: String) {
println!("took: {}", text); // text is dropped (freed) at the end here
}
fn makes_copy(num: i32) {
println!("copy of: {}", num);
}
fn gives_ownership() -> String {
String::from("a fresh string") // ownership moves out to the caller
}When takes_ownership ends, its text goes out of scope and is automatically dropped — its heap memory freed, no manual cleanup. This automatic drop at end-of-scope is the third ownership rule in action.
Your turn. Fill in the blanks marked ___, then run it.
// 🎯 YOUR TURN — fix the moves so the program compiles, then run it.
fn main() {
let original = String::from("data");
// 1) We want to KEEP using original after making a copy.
// Use the method that makes a deep copy.
let copy = original.___(); // 👉 the deep-copy method
println!("original = {}", original); // must still work!
println!("copy = {}", copy);
// 2) i32 is a Copy type, so this already works — just fill the print.
let a = 42;
let b = a;
println!("a = {}, b = {}", a, ___); // 👉 the second variable
}
// ✅ Expected output:
// original = data
// copy = data
// a = 42, b = 42Mini-Challenge: Ownership Round-Trip
Move a String into a function and get a new one back. Run it with cargo run and check the output.
// 🎯 MINI-CHALLENGE: ownership round-trip.
//
// 1. Write fn shout(text: String) -> String that returns text + "!".
// (Use format!("{}!", text) to build the new String.)
// 2. In main, make a String "go" and pass it to shout, capturing the result.
// 3. Print the result. Because ownership moved INTO shout and a NEW String
// came back, plan your variables so the program compiles.
//
// ✅ Example output:
// go!
fn main() {
// your code here
}Common Errors (and the fix)
- "borrow of moved value" — You used a variable after it was moved (assigned away or passed to a function). Either .clone() it, or restructure so you don't need the original afterward.
- "value used here after move" — Same cause. The compiler even points at the move and the later use. Borrow it instead (next lesson) or clone.
- Cloning everything to silence errors — .clone() works but copies data. Often the real fix is to borrow with & instead.
- Returning a reference to a local — You can't return a reference to a value owned by the function; return the owned value (move it out) instead.
Pro Tips
- 💡 Reach for borrowing first: if you only need to read a value, borrow it with & rather than moving or cloning.
- 💡 Clone is a tool, not a crime: in non-hot code, a clone for clarity is perfectly fine — just be intentional.
- 💡 Trust the drop: you never call free; Rust drops values automatically when their owner leaves scope.
📋 Quick Reference — Ownership
| Concept | Behaviour |
|---|---|
| Move | let b = a; // a invalid |
| Clone | let b = a.clone(); |
| Copy type | let b = a; // both valid |
| Move into fn | f(s); // s moved |
| Return moves out | -> String |
🎉 Lesson Complete!
- ✅ Each value has one owner; ownership moves on assignment or function call
- ✅ Using a moved value is a compile error
- ✅ .clone() deep-copies; Copy types like i32 duplicate cheaply
- ✅ Values are dropped (freed) automatically when their owner leaves scope
Practice quiz
How many owners does a value have at one time in Rust?
- Zero
- Two
- Exactly one
- Unlimited
Answer: Exactly one. Each value has exactly one owner at a time.
After let s2 = s1; for a String, what happens to s1?
- It is moved and becomes invalid
- It is copied
- It is borrowed
- It stays usable
Answer: It is moved and becomes invalid. Assigning a String moves ownership, leaving s1 invalid.
What does using a moved String afterward cause?
- A runtime crash
- A silent copy
- Nothing
- A compile error
Answer: A compile error. Using a moved value is a compile-time error (E0382).
How do you make an independent deep copy of a String?
- .copy()
- .clone()
- .dup()
- .deep()
Answer: .clone(). .clone() deep-copies the heap data so both variables are valid.
Why can you use an i32 after let y = x;?
- i32 implements Copy
- i32 is on the heap
- Integers cannot move
- x is borrowed
Answer: i32 implements Copy. Small stack types like i32 implement Copy, so both stay valid.
What happens when you pass a String to a function by value?
- It is borrowed
- It is cloned
- It is moved into the function
- Nothing changes
Answer: It is moved into the function. Passing a String by value moves ownership into the function.
When is a value dropped (its memory freed)?
- When you call free()
- When its owner goes out of scope
- Never
- At program start
Answer: When its owner goes out of scope. A value is dropped automatically when its owner leaves scope.
How can a function give ownership back to the caller?
- By cloning
- By borrowing
- It cannot
- By returning the value
Answer: By returning the value. Returning a value moves ownership out to the caller.
Which is often a better fix than cloning to read a value?
- Copy the whole program
- Borrow it with &
- Use unsafe
- Make it static
Answer: Borrow it with &. If you only need to read, borrow with & instead of cloning or moving.
Does Rust use a garbage collector to free memory?
- Yes
- Only in debug
- No, ownership frees memory at compile-time-known points
- Only for Strings
Answer: No, ownership frees memory at compile-time-known points. Ownership frees memory deterministically with no garbage collector.
Continue this course
- Previous: Control Flow
- Next: References & Borrowing — Borrow values with & and &mut without giving up ownership
- Quick reference: Rust cheat sheet › Ownership & Borrowing
Frequently asked questions
What are the three rules of ownership in Rust?
First, each value in Rust has a single owner — a variable that is responsible for it. Second, there can be only one owner at a time; assigning or passing the value moves ownership to a new owner. Third, when the owner goes out of scope, the value is automatically dropped and its memory is freed. These three rules let Rust manage memory safely at compile time, with no garbage collector and no manual free calls.
What does it mean that a value was 'moved'?
For heap-allocated types like String, assigning one variable to another (let s2 = s1) transfers ownership rather than copying the data. After the move, the original variable s1 is no longer valid, and using it is a compile error. This prevents two variables from both thinking they own the same memory, which would otherwise lead to double-frees. If you genuinely want two independent copies, call .clone() to deep-copy the data.
Why can I use an i32 after assigning it but not a String?
Small, fixed-size types that live entirely on the stack — like integers, floats, booleans, and chars — implement the Copy trait, so assigning them makes a quick bitwise copy and both variables stay valid. Types that own heap data, like String and Vec, do not implement Copy because copying them would be expensive and ambiguous, so they move instead. That's why 'let y = x' keeps x usable for an i32 but invalidates x for a String.
Isn't moving everything into functions inconvenient?
It would be, which is exactly why Rust has borrowing. Instead of moving a value into a function and getting it back, you can lend a reference with & so the function can read (or with &mut, modify) the value without taking ownership. Borrowing is the subject of the next lesson, and in real code you borrow far more often than you move.