Ownership rules summary cheat sheet

Rust ownership rules ensure memory safety by assigning a single owner to each value, transferring ownership on assignment, and automatically freeing memory when the owner goes out of scope.

Ownership rules summary cheat sheet

You write a function that takes a String. You call it. You try to print the string afterwards. The compiler stops you. You try to pass two mutable references to the same data. The compiler stops you again. You're not making a logic error. You're bumping into Rust's ownership system. This system guarantees memory safety without a garbage collector. It does so by enforcing strict rules about who owns data and how long it lives. Here are the rules, explained clearly.

The core contract

Ownership is the contract that every value has exactly one owner at any moment. The owner is responsible for cleaning up the value when it goes out of scope. This contract prevents double-free bugs and data races. It also determines when memory is freed and when resources are released.

Think of a value like a physical key to a server room. Only one person can hold the key. If you hand the key to a colleague, you no longer have it. You can't open the door anymore. If the colleague leaves the building, they must return the key or destroy it. The key tracks responsibility. Rust's ownership works the same way for memory.

Rule one: one owner per value

When you create a value, the variable holding it becomes the owner. If you assign that value to another variable, ownership moves. The original variable becomes invalid. This is called a move.

fn main() {
    // Allocate "hello" on the heap. s1 holds the pointer, length, and capacity.
    // s1 is the sole owner of this heap memory.
    let s1 = String::from("hello");

    // Copy the pointer, length, and capacity to s2.
    // Rust invalidates s1 to prevent two owners from freeing the same memory.
    let s2 = s1;

    // s2 is the owner now. Printing works fine.
    println!("{}", s2);

    // This line would cause a compile error. s1 was moved and is no longer valid.
    // println!("{}", s1);
}

When you assign s2 = s1, Rust doesn't copy the string data. It copies the small struct on the stack that points to the heap data. Then it marks s1 as moved. The compiler tracks this state. If you try to use s1 after the move, you get E0382 (use of moved value). When s2 goes out of scope, Rust calls drop on the string, freeing the heap memory. Since s1 is invalid, nothing else tries to free that memory. No double-free.

The compiler tracks ownership like a ledger. If the books don't balance, the code doesn't run.

Rule two: borrowing allows access without ownership

Moving ownership everywhere makes code clumsy. You often need to pass data to a function without giving up ownership. Rust solves this with borrowing. A borrow is a reference to the data. The borrower can use the data, but the owner keeps responsibility.

There are two kinds of borrows. An immutable borrow uses &T. A mutable borrow uses &mut T. The rules for borrowing are strict. You can have many immutable borrows at the same time. You can have exactly one mutable borrow. You can never have a mutable borrow and an immutable borrow active simultaneously.

/// Reads data without taking ownership.
/// The caller retains ownership and can reuse `data` after this call.
fn read_data(data: &String) {
    println!("Reading: {}", data);
}

/// Modifies data in place.
/// Requires exclusive access. No other references can exist during this call.
fn update_data(data: &mut String) {
    data.push_str(" updated");
}

fn main() {
    let mut text = String::from("draft");

    // Immutable borrow. Multiple immutable borrows are allowed.
    read_data(&text);
    read_data(&text);

    // Mutable borrow. Requires exclusive access.
    // All previous immutable borrows must end before this call.
    update_data(&mut text);

    // Ownership never left `text`. We can still use it.
    println!("{}", text);
}

If you try to create a mutable reference while an immutable reference is still active, the compiler rejects this with E0502 (cannot borrow as mutable because it is also borrowed as immutable). Rust prevents data races at compile time. You can't read and write the same data simultaneously. This rule holds even in single-threaded code. It keeps the mental model simple and safe.

Prefer references over ownership in function signatures unless the function needs to keep the data. It makes your API more flexible and lets callers reuse values.

Borrowing is the way to share data without giving it away. Use it liberally.

Rule three: copy types skip the move

Not every assignment moves ownership. Some types implement the Copy trait. For these types, assignment copies the data instead of moving it. The original variable remains valid.

Primitive types like i32, bool, f64, and char are Copy. These types have a known size at compile time and live entirely on the stack. Copying them is cheap and safe.

fn main() {
    // x is an i32, which implements Copy.
    let x = 42;

    // Assignment copies the value. x remains valid.
    let y = x;

    // Both x and y are usable.
    println!("x = {}, y = {}", x, y);
}

When you assign y = x, Rust copies the bits of x into y. Both variables hold independent values. Changing y doesn't affect x. This behavior matches what you expect from integers in most languages.

String is not Copy. It owns heap memory. Copying the heap data on every assignment would be slow. Rust chooses to move the pointer instead. If you need a copy of a String, call clone() explicitly.

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

    // Explicitly copy the data. s1 remains valid.
    let s2 = s1.clone();

    println!("s1 = {}, s2 = {}", s1, s2);
}

Derive Copy for structs that only contain Copy fields. It saves boilerplate and makes the type behave like a primitive.

#[derive(Copy, Clone)]
struct Point {
    x: f64,
    y: f64,
}

fn main() {
    let p1 = Point { x: 1.0, y: 2.0 };
    let p2 = p1; // Copy, not move.
    println!("{}, {}", p1.x, p2.x); // Both valid.
}

If it fits on the stack and is cheap to duplicate, make it Copy.

Scope and drop

When the owner goes out of scope, Rust cleans up the value. This happens automatically. Rust calls the drop method at the end of the scope. For String, this frees the heap memory. For files, this closes the handle. For network sockets, this closes the connection. You rarely write drop manually. The compiler inserts it automatically.

fn main() {
    // s is created here.
    let s = String::from("temporary");

    // s is used here.
    println!("{}", s);

    // s goes out of scope here.
    // Rust calls drop automatically. Memory is freed.
}

Rust uses Non-Lexical Lifetimes. The compiler tracks the last use of a reference. You don't need to wrap code in braces to shorten a borrow. The compiler is smart enough to know when a reference is dead. Write clean code. Let the compiler manage lifetimes.

Don't create artificial scopes to manage borrows. Let the compiler do its job. Artificial scopes make code harder to read.

Common pitfalls and errors

You try to use a variable after passing it to a function. The compiler rejects this with E0382 (use of moved value). The fix is to either clone the value if you need a copy, or pass a reference if the function doesn't need ownership.

You try to create a mutable reference while an immutable reference is still active. The compiler rejects this with E0502 (cannot borrow as mutable because it is also borrowed as immutable). Rust prevents data races at compile time. You can't read and write the same data simultaneously.

You try to modify a variable declared with let instead of let mut. The compiler rejects this with E0596 (cannot borrow as mutable). Variables are immutable by default in Rust. Add mut to the binding to allow changes.

You try to dereference a raw pointer in safe code. The compiler rejects this with E0133 (dereference of raw pointer requires unsafe). Raw pointers bypass ownership checks. You must wrap the operation in an unsafe block and prove safety.

Read the error code. E0382 means you moved a value. Fix the move, don't fight the compiler.

When to use what

Use ownership transfer when the function takes responsibility for the data and the caller has no further use for it. Use immutable references &T when the function reads the data and the caller must retain ownership. Use mutable references &mut T when the function modifies the data in place and the caller wants the result. Use clone() when you need a separate copy of the data and the performance cost is acceptable. Use Rc<T> or Arc<T> when multiple parts of the program must own the same data simultaneously.

Pick the smallest permission you need. Ownership is heavy. References are light.

Where to go next