Skip to content
C Codeloom
Rust

Rust Ownership: The Idea That Makes Rust Different

A friendly tour of Rust's ownership system — the three rules, move semantics, Copy vs Clone, borrowing with & and &mut, the borrow checker, and the beginner errors you'll see first.

·10 min read · By Yash Kesharwani
Beginner 13 min read

What you'll learn

  • The three rules of ownership, plain English
  • What "moving" a value means and why assignment can take it away
  • The difference between Copy and Clone — and which types are which
  • How to borrow a value with & and &mut without giving it up
  • What the borrow checker is doing, and how to read its errors
  • The four classic beginner mistakes and how to fix each one

Prerequisites

Every other language in mainstream use manages memory in one of two ways. Either it has a garbage collector that scans your heap periodically and frees what’s no longer reachable (Python, Java, Go, JavaScript), or it makes you free memory yourself and hopes you don’t make a mistake (C, C++).

Rust takes a third path: the compiler tracks who owns each piece of memory and frees it for you at exactly the right moment. The rules that make this possible are called ownership, and they are the thing that makes Rust different from anything else you’ve used.

This post walks through them gently. Expect to read this more than once. That is normal.

The three rules

The whole system is summarised in three sentences:

  1. Every value in Rust has a single owner.
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value is dropped (freed).

A value here means a piece of data — a string, a vector, a struct, a number. The owner is the variable name bound to it. “Out of scope” means the closing brace of the block where the variable was declared.

fn main() {
    let s = String::from("hello");   // s owns the string
    println!("{}", s);
}                                    // s goes out of scope, string is freed

There is no garbage collector running. The compiler inserted the equivalent of a free call at the closing brace, because it could see that s’s scope ended there. No leak, no double-free, no work for you.

Move semantics

This is where Rust starts to feel new. Assigning one variable to another does not always copy. For heap-allocated types, it moves ownership:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;             // ownership moves to s2

    println!("{}", s2);      // ok
    println!("{}", s1);      // error: value borrowed after move
}

The compiler refuses the last line:

cargo run
// output:
// error[E0382]: borrow of moved value: `s1`

The reason: if both s1 and s2 were still valid owners, Rust would have two pointers to the same heap buffer. When s1 went out of scope, the buffer would be freed. When s2 went out of scope, it would be freed again — a double-free. Rust avoids the bug entirely by invalidating s1 at the move site.

Function calls move too:

fn take(s: String) {
    println!("got: {}", s);
}

fn main() {
    let s = String::from("hello");
    take(s);
    println!("{}", s);   // error: s was moved into take()
}

If take needs the value, the move makes sense — the function is now the owner and will drop the string when it returns. If take only needs to read the value, the right tool is borrowing, which we cover below.

Copy types

Not everything moves. Simple scalar types — i32, f64, bool, char, and tuples of those — implement the Copy trait, which means assignment makes a bit-for-bit copy and both variables remain usable:

let a = 5;
let b = a;                  // a is copied, not moved
println!("{} {}", a, b);    // both ok — output: 5 5

The rule of thumb: types that live entirely on the stack are Copy; types that own heap data are not. String, Vec<T>, Box<T>, and most structs are not Copy.

You cannot implement Copy for a type that contains a non-Copy field. The compiler stops you, because copying a String bit-for-bit would create two owners of the same buffer.

Clone, for an explicit deep copy

When you actually want to duplicate a heap-owning value, call .clone():

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();          // explicit deep copy

    println!("{} {}", s1, s2);
    // output: hello hello
}

Both s1 and s2 now own separate buffers. Cloning is allowed but it is not free — there is a heap allocation and a memcpy involved. Rust makes you spell it out so you notice the cost.

Clone is for explicit duplication; Copy is for cheap, implicit duplication of small stack values. Most of your types will be neither, and you will reach for borrowing instead.

Borrowing with &

You usually don’t want to move or clone — you want to let a function read a value without taking it. That is a borrow, written with &:

fn length(s: &String) -> usize {
    s.len()
}

fn main() {
    let s = String::from("hello");
    let n = length(&s);
    println!("{} has length {}", s, n);   // s is still valid
    // output: hello has length 5
}

The & creates an immutable reference — a pointer that does not own. When it goes out of scope, nothing is freed. A reference is read-only and cannot outlive its owner; the compiler tracks both.

Borrowing with &mut

To borrow mutably, use &mut:

fn push_world(s: &mut String) {
    s.push_str(", world");
}

fn main() {
    let mut s = String::from("hello");
    push_world(&mut s);
    println!("{}", s);
    // output: hello, world
}

Three things had to be true here. The owner s was declared mut. The function parameter was &mut String. The call site passed &mut s. All three are required — Rust never lets mutability sneak in.

The single most important rule in the borrow checker is this:

At any given time, you can have either one mutable reference or any number of immutable references. Not both.

This is the rule that prevents data races at compile time. If only one writer can exist at a time, and writers exclude readers, then no two threads can race on the same memory.

fn main() {
    let mut s = String::from("hello");

    let r1 = &s;
    let r2 = &s;        // ok: two immutable refs

    let r3 = &mut s;    // error: cannot borrow as mutable while immutable refs exist
    println!("{} {} {}", r1, r2, r3);
}

The fix is usually to scope the references so they don’t overlap:

fn main() {
    let mut s = String::from("hello");

    {
        let r1 = &s;
        println!("{}", r1);
    }   // r1 goes out of scope here

    let r3 = &mut s;
    r3.push('!');
    println!("{}", r3);
    // output: hello!
}

The borrow checker, plainly

The borrow checker is the compiler pass that enforces the rules above. Its job is to ensure that every reference is valid for as long as you use it, and that mutable and immutable references never coexist.

It will refuse code that would be safe in some runs, because it cannot prove safety in all runs. This is the trade-off Rust makes. You get safety at compile time; you pay with a stricter language.

When you read a borrow-checker error, look for two things:

  1. Where was the value created or last borrowed? The compiler tells you in the error.
  2. What scope ends where? Closing braces are the events that drop owners and end references.

Read the error in full. Rust’s error messages are unusually good — they often include the exact fix as a suggestion.

Four classic beginner errors

These will trip you in your first week. Each has a clear fix once you know the pattern.

1. “Borrow of moved value”

let s = String::from("hi");
let _t = s;
println!("{}", s);     // error

You moved s into _t. Either don’t move it (use &s), clone it (s.clone()), or restructure so you don’t need s after.

2. “Cannot borrow as mutable, as it is not declared mutable”

let s = String::from("hi");
s.push('!');           // error

The owner needs mut: let mut s = ....

3. “Cannot borrow as mutable more than once”

let mut s = String::from("hi");
let a = &mut s;
let b = &mut s;        // error

You tried to hand out two mutable references at once. Use the first, let it go out of scope, then take the second.

4. “Returns a reference to data owned by the current function”

fn dangle() -> &String {     // error
    let s = String::from("hi");
    &s
}

s is dropped when dangle returns, so the reference would dangle. Return the owned String instead — the caller becomes the new owner. Most “lifetime” errors at the boundary of a function are solved by returning ownership rather than a reference.

Try this. Write a function prepend_hello(s: &mut String) that prepends "Hello, " to the front of s in place. Then write with_hello(s: &str) -> String that returns a new String with "Hello, " prepended, without touching the original. Call both from main and notice the difference: one needs &mut on a mut binding, the other takes a borrow and returns ownership.

A worked example

A small program that exercises ownership, borrowing, and mutation:

fn capitalize(s: &mut String) {
    if let Some(c) = s.get_mut(0..1) {
        c.make_ascii_uppercase();
    }
}

fn main() {
    let mut greeting = String::from("hello");
    capitalize(&mut greeting);
    println!("{}", greeting);
    // output: Hello
}

capitalize takes &mut String and mutates in place; main keeps ownership throughout.

Why the rules pay off

The first week of fighting the borrow checker is real. Most Rust programmers describe it the same way: a frustration that gives way to a quiet confidence that programs that compile actually work. No null-pointer crashes, no iterator invalidation, no data races on shared state. The compiler caught them before the test suite ever ran.

The trade is the time you spend learning the rules. The pay-off is the time you stop spending in a debugger.

Recap

You now know:

  • The three rules: one owner at a time, ownership moves on assignment or call, the value is dropped when the owner leaves scope.
  • Move semantics invalidate the source binding; Copy types skip the move by being cheap to duplicate; Clone is an explicit deep copy you have to ask for.
  • &T borrows immutably (many at once allowed); &mut T borrows mutably (only one allowed, and not while any immutable borrows exist).
  • The borrow checker enforces these rules at compile time, with error messages that often suggest the fix.
  • The four classic beginner errors — moved value, missing mut, double mutable borrow, dangling return — each have a clean idiomatic fix.

Next steps

You now have the four pillars of Rust: variables and types, functions, and ownership. The next posts in the series build on this foundation — structs and enums for grouping data, pattern matching, error handling with Result, and the iterator chain that powers idiomatic Rust code.

Until then, the best thing you can do is open the Rust playground and write small programs that move, borrow, and clone. The rules become reflex through repetition, not reading.

Revisit What Is Rust? if you want a reminder of why any of this is worth the effort. The short answer: programs that compile are programs that work.

Questions or feedback? Email codeloomdevv@gmail.com.