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

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 = 42

Mini-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)

Pro Tips

📋 Quick Reference — Ownership

ConceptBehaviour
Movelet b = a; // a invalid
Clonelet b = a.clone();
Copy typelet b = a; // both valid
Move into fnf(s); // s moved
Return moves out-> String

🎉 Lesson Complete!

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

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.