Code of the Day
BeginnerCore syntax

Ownership basics

Rust's ownership system — the rule that gives you memory safety without a garbage collector — explained from the stack up.

RustBeginner14 min read
Recommended first
By the end of this lesson you will be able to:
  • Explain what the stack and heap are and which Rust types use each
  • State Rust's core ownership rule and the three sub-rules that follow from it
  • Describe what "move semantics" means and why moved values can no longer be used
  • Use clone() to make an explicit deep copy when you need to keep both values
  • Understand what the borrow checker is and what it protects against

Every programming language has to answer the same question: when you're done with a piece of memory, how does the program reclaim it? languages (Python, JavaScript, Go) scan for unreachable values at runtime. Languages like C leave it entirely to you — and you will sometimes forget. Rust takes a third path: the compiler tracks statically, at compile time, and inserts the frees for you. No runtime cost, no .

This is the idea that makes Rust unique. It's also the idea that makes the compiler push back on you more than you might expect. This lesson explains the core rule; the rest of the language clicks into place once you have it.

Stack and heap: a quick mental model

The fundamentals track covers memory in detail. A quick refresher for Rust's purposes:

  • The is fast, fixed-size storage. Every local variable of a known, fixed size lives here. When a function returns, its entire stack frame disappears automatically.
  • The is slower, dynamically-sized storage. Data whose size isn't known at compile time — like a String that can grow — lives on the heap.

Rust's scalar types (i32, f64, bool, char) are small and fixed-size, so they live on the stack. A String stores its text on the heap and keeps a pointer (plus length and capacity) on the stack.

This distinction matters because heap data doesn't automatically disappear when a function returns — it has to be explicitly freed. Ownership is how Rust automates that.

The ownership rule

Rust enforces one central rule:

Each value has exactly one owner. When the owner goes out of scope, the value is dropped.

"Dropped" means the memory is freed. No garbage collector needed — the compiler inserts the drop at the end of the owning scope.

Two important consequences:

  1. There can only be one owner at a time. You cannot have two variables that both own the same heap allocation.
  2. Ownership can be transferred. When you do, the original owner can no longer be used.

Move semantics

This is where new Rust programmers usually hit their first real compiler error. Try this:

let s1 = String::from("hello");
let s2 = s1;            // ownership moves to s2
println!("{}", s1);     // compiler error!
error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:4:20
  |
2 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`
3 |     let s2 = s1;
  |              -- value moved here
4 |     println!("{}", s1);
  |                    ^^ value borrowed here after move

After let s2 = s1, ownership of the String has moved to s2. Using s1 after that is a compile error. There is still only one owner (s2) and when s2 goes out of scope, the String will be freed exactly once.

This might feel strange at first. In Python, s2 = s1 would give you two names pointing at the same object. In Rust, s1 is gone — the compiler prevents you from using a moved value.

Why no ? If let s2 = s1 made a shallow copy, you'd have two owners of the same heap data. When both went out of scope, both would try to free the same memory — a "" bug, which leads to crashes and security vulnerabilities. Rust makes it a compile error instead.

Copy types: scalars are different

Stack-only types implement a called Copy. For them, assignment makes a bitwise copy — no move, no ownership transfer:

let x = 5;
let y = x;           // x is copied, not moved
println!("{}", x);   // still valid!

i32, f64, bool, char, and tuples of Copy types all implement Copy. String does not — strings are heap-allocated, so assignment is a move.

clone() — explicit deep copy

When you genuinely need two independent owned String values, call .clone():

let s1 = String::from("hello");
let s2 = s1.clone();    // deep copy (new allocation, new owner)
println!("s1={}, s2={}", s1, s2);  // both valid

.clone() makes a — a completely new heap allocation with the same content. It's intentionally verbose: a signal that says "this costs memory." That's the kind of cost you want to see, not hide.

Don't reach for clone() to fix every borrow error. It works, but it's often not the right answer — it allocates. The idiomatic solution is usually borrowing (covered in the intermediate track). For now, use clone() when you understand that you want two separate copies.

The borrow checker

The ownership rule is enforced by the , the part of the compiler that tracks who owns what. When you see an error like value moved here or cannot borrow x as mutable because it is also borrowed as immutable, that's the borrow checker.

The borrow checker can feel adversarial at first. Experienced Rust programmers describe the experience differently: the compiler is finding a real problem in your mental model. Fighting it less and reading its messages more closely is the fastest path forward.

Ownership and functions

Passing a String to a function moves ownership into the function:

fn print_greeting(s: String) {
    println!("Hello, {}", s);
}   // s is dropped here

fn main() {
    let name = String::from("Ada");
    print_greeting(name);
    // name is no longer valid here — it was moved
}

This is surprising but consistent: the same ownership rule applies everywhere. The intermediate track covers references (&String) — a way to let functions look at a value without taking ownership of it. That's borrowing, and it's the idiomatic solution for most cases.

Check your understanding

Knowledge check

  1. 1.
    What happens when the owner of a value goes out of scope in Rust?
  2. 2.
    After let s2 = s1; where s1 is a String, what happens if you use s1?
  3. 3.
    Which of the following types implement Copy in Rust?
  4. 4.
    Calling .clone() on a String allocates new heap memory.

When the agent's away

The borrow checker errors you'll encounter most often:

  • use of moved value — you used a variable after its value was moved somewhere else. Either clone() before the move, or redesign to avoid needing both.
  • cannot borrow as mutable — you're trying to mutate something that isn't mut, or that has active borrows. Check for mut first; borrow issues will make more sense after the intermediate track.
  • does not live long enough — a reference is trying to outlive the value it points to. The compiler is preventing a dangling pointer.

Read the error, look at the line numbers it points to, and trust that the compiler is right about the problem even when it isn't immediately obvious why.

Where to go next

Ownership is the central idea of Rust. Everything else — references, lifetimes, smart pointers — builds on this foundation. Before going further, practice: write a few small programs that move values between functions, observe the errors, and fix them. Next: the lab puts the whole core syntax module together.

Finished reading? Mark it complete to track your progress.

On this page