Skip to content

Rust Ownership

Ownership is the idea that makes Rust Rust. It’s how the language guarantees memory safety without a garbage collector and without you manually freeing memory. It feels unusual at first, but the core idea is simple. Let’s build it up slowly.

When a program runs, it stores data in memory. At some point that memory needs to be cleaned up, or your program leaks memory and eventually crashes. Languages handle this in two common ways:

  • Manual (C/C++): you free memory yourself. Forget to, and you leak. Free too early, and you get crashes and security bugs.
  • Garbage collector (Java, Go, Python): a background process cleans up for you. Convenient, but it costs performance and predictability.

Rust takes a third path: it decides at compile time exactly when each value should be cleaned up, using a set of rules about ownership. No manual freeing, no garbage collector.

These are the whole system. Everything else follows from them:

  1. Each value has a single owner (a variable that “owns” it).
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value is automatically cleaned up.

Let’s see each rule in action.

Rule 3: values are dropped at the end of scope

Section titled “Rule 3: values are dropped at the end of scope”

A scope is the region between { and }. When a variable’s scope ends, Rust automatically frees its value — you never write free():

fn main() {
{
let message = String::from("hello"); // message is valid here
println!("{message}");
} // <- scope ends, `message` is automatically cleaned up
// println!("{message}"); // ❌ error: message no longer exists here
}
▶ Try it Yourself

You didn’t have to free anything — Rust did it the moment message went out of scope.

Here’s where it gets interesting. Watch what happens when you assign one variable to another:

fn main() {
let s1 = String::from("hello");
let s2 = s1; // ownership MOVES from s1 to s2
println!("{s2}"); // ✅ works
// println!("{s1}"); // ❌ error: s1 was moved
}
▶ Try it Yourself

When you write let s2 = s1, Rust doesn’t copy the string — it moves ownership from s1 to s2. Because rule 2 says there can only be one owner, s1 is now invalid. Trying to use it gives:

error[E0382]: borrow of moved value: `s1`

If you actually want a second, independent copy, call .clone():

fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // makes a full copy
println!("s1 = {s1}, s2 = {s2}"); // ✅ both work now
}
▶ Try it Yourself

clone() is explicit on purpose — copying can be expensive, so Rust makes you ask for it.

You might wonder why this doesn’t cause an error:

fn main() {
let x = 5;
let y = x;
println!("x = {x}, y = {y}"); // ✅ both work
}

Small, fixed-size values like integers, floats, booleans, and char are cheap to copy, so Rust just copies them instead of moving. As a beginner, the rule of thumb is:

  • Simple values (numbers, bool, char) → copied. The original stays valid.
  • Owned data (like String) → moved. The original becomes invalid.

Passing a value to a function moves it too — just like assigning to a variable:

fn take_ownership(s: String) {
println!("I now own: {s}");
} // s is dropped here
fn main() {
let text = String::from("hello");
take_ownership(text);
// println!("{text}"); // ❌ error: text was moved into the function
}

This is exactly why the next topic — borrowing — exists. Borrowing lets a function use a value without taking ownership of it, so you can keep using it afterward.

Try each one yourself first, in the Rust Playground or a local project. Compiler errors are expected along the way: read them, they usually point right at the move.

This program doesn’t compile. Fix it so it prints draft -> draft v2, without removing either variable.

fn main() {
let original = String::from("draft");
let mut edited = original;
edited.push_str(" v2");
println!("{original} -> {edited}");
}
error[E0382]: borrow of moved value: `original`
--> src/main.rs:5:16
|
2 | let original = String::from("draft");
| -------- move occurs because `original` has type `String`, which does not implement the `Copy` trait
3 | let mut edited = original;
| -------- value moved here
4 | edited.push_str(" v2");
5 | println!("{original} -> {edited}");
| ^^^^^^^^ value borrowed here after move

Hint: you need two independent strings, so that changing one doesn’t touch the other.

Show solution
fn main() {
let original = String::from("draft");
let mut edited = original.clone(); // a full, separate copy
edited.push_str(" v2");
println!("{original} -> {edited}");
}

Output:

draft -> draft v2
▶ Try it Yourself

let mut edited = original; moves the string, so original is no longer valid. .clone() makes a second string with its own memory. Now push_str changes only edited, and original still says draft.

Write a function add_signature that takes ownership of a String, adds " -- Ferris" to the end, and hands the string back. Call it from main and print the result.

Hint: a function can return the value it was given. Write -> String in the signature, and take the parameter as mut text: String so you’re allowed to change it.

Show solution
fn add_signature(mut text: String) -> String {
text.push_str(" -- Ferris");
text // ownership moves back out to the caller
}
fn main() {
let letter = String::from("See you soon!");
let letter = add_signature(letter);
println!("{letter}");
}

Output:

See you soon! -- Ferris
▶ Try it Yourself

Ownership moves into the function with the call, and moves back out with the return value. We catch it in a new letter variable (shadowing the old one). This works, but passing values in and out all the time gets tiring. That’s exactly the problem borrowing solves.

This program doesn’t compile. Fix it so it prints Ana joined the list and then ["Ana"].

fn main() {
let mut names = Vec::new();
let name = String::from("Ana");
names.push(name);
println!("{name} joined the list");
println!("{:?}", names);
}
error[E0382]: borrow of moved value: `name`
--> src/main.rs:5:16
|
3 | let name = String::from("Ana");
| ---- move occurs because `name` has type `String`, which does not implement the `Copy` trait
4 | names.push(name);
| ---- value moved here
5 | println!("{name} joined the list");
| ^^^^ value borrowed here after move

Hint: push takes ownership, just like a function call. Who owns the string after line 4? Either use name before the move, or ask the new owner.

Show solution
fn main() {
let mut names = Vec::new();
let name = String::from("Ana");
println!("{name} joined the list"); // use it while we still own it
names.push(name); // now the vector owns it
println!("{:?}", names);
}

Output:

Ana joined the list
["Ana"]
▶ Try it Yourself

After names.push(name), the vector is the owner, and name can’t be used anymore. Printing first is one fix. Another is to keep the original order and print names[0] instead, because the vector still has the string. Cloning would also work, but there’s no need to copy here.

  • Ownership lets Rust manage memory safely with no garbage collector.
  • Three rules: one owner per value, one owner at a time, value dropped when the owner leaves scope.
  • Assigning owned data (like String) moves it — the original becomes invalid.
  • Use .clone() for an independent copy; simple types (numbers, bool, char) are copied automatically.
  • Passing a value to a function moves it too — which leads us to borrowing.

Quick check

1. How many owners can a value have at one time in Rust?

2. What method do you call to make an independent copy of a String instead of moving it?

3. Which of these types is copied automatically instead of moved when assigned to another variable?

4. What happens when you pass a String to a function that takes it by value (not by reference)?

5. When does Rust automatically clean up (drop) a value, according to this page?

Score: 0 / 5