Andrew Mercer
on this page

Understanding Ownership, Moves, and Borrowing

Every program has to answer three questions about its data: where does it live, who is allowed to use it, and when is it cleaned up?

Garbage-collected languages (Python, Go, Java) answer the last question at runtime: a collector periodically finds values nothing refers to and frees them. C and C++ leave it largely to the programmer. Rust takes a third route called ownership: a small set of rules, checked by the compiler, that decide when memory is freed and who may touch it. Get them right and you get memory safety with no garbage collector. Get them wrong and the program doesn't compile.

This page builds the model up from nothing. If you have a specific use of partially moved value error in front of you, the companion page Fixing "use of partially moved value" walks through one in detail.

The three ownership rules

Everything else on this page follows from these:

  1. Every value has exactly one owner (a variable, a struct field, a collection slot, …).
  2. Ownership can be transferred (moved), but there is still only one owner at a time.
  3. When the owner goes out of scope, the value is dropped: its destructor runs and its memory is freed.

An analogy helps. Lending a friend a book is different from giving it away:

  • Give it away → it's theirs now. That's a move.
  • Lend it → you still own it; they get to use it for a while and must give it back. That's a borrow.

Scope and drop

A value lives until the end of the block that owns it:

fn main() {
    {
        let greeting = String::from("hello"); // `greeting` owns a heap buffer
        println!("{greeting}");
    } // <- `greeting` goes out of scope; the String is dropped and its memory freed

    // println!("{greeting}"); // error[E0425]: cannot find value `greeting`
}

Dropping is deterministic. You know exactly where cleanup happens: at the closing brace. Rust uses the same mechanism for any resource, not just memory. A File closes itself, a MutexGuard unlocks itself, a database connection returns to its pool, all when the owner goes out of scope. This pattern is called RAII (Resource Acquisition Is Initialization), and it's why Rust code rarely needs finally blocks or defer.

Moving: transferring ownership

Assigning a value that owns resources to another variable moves it:

let first = String::from("hello");
let second = first;            // ownership moves to `second`

println!("{second}");          // fine
// println!("{first}");        // error[E0382]: borrow of moved value: `first`

Why forbid using first? A String is three machine words on the stack (pointer, length, capacity) pointing at a buffer on the heap. let second = first copies those three words. If both variables stayed usable, both would think they own the buffer, and both would try to free it at the end of scope, a double free. Rust prevents that by declaring first dead after the move. No data was duplicated; responsibility changed hands.

Moves into and out of functions

Passing a value to a function moves it, exactly like assignment. Returning a value moves it back out:

fn shout(s: String) -> String {
    s.to_uppercase()
} // `s` is dropped here (to_uppercase built a new String)

fn main() {
    let word = String::from("hi");
    let loud = shout(word);    // `word` moved into `shout`
    println!("{loud}");        // "HI"
    // println!("{word}");     // error[E0382]: `word` was moved
}

Moves also happen in less obvious places:

  • for item in vec consumes the vector (use for item in &vec to borrow it).
  • Closures declared with move |…| take ownership of what they capture.
  • Destructuring patterns (let (a, b) = pair;, match arms) can move fields out of a larger value.
  • Methods that take self (not &self) consume the receiver, e.g. Option::unwrap, Vec::into_iter, String::into_bytes.

Borrowing: access without ownership

Moving a value into a function just to read it, and then moving it back, would be tedious. Instead you borrow it by creating a reference with &:

fn len_of(s: &str) -> usize {
    s.len()
} // nothing is dropped here: `len_of` never owned the string

fn main() {
    let name = String::from("Ada");
    let n = len_of(&name);     // lend `name` to the function
    println!("{name} has {n} bytes"); // `name` is still usable
}

There are two kinds of reference:

Reference Name What you can do
&T shared (immutable) reference Read. Any number may exist at once.
&mut T exclusive (mutable) reference Read and write. Only one may exist at a time.
fn bump(score: &mut i32) {
    *score += 1;               // `*` dereferences: "the value behind this reference"
}

fn main() {
    let mut score = 10;        // the binding itself must be `mut` to lend it mutably
    bump(&mut score);
    println!("{score}");       // 11
}

The borrowing rules

At any given point in the program, for a given value you may have either:

  • any number of shared references (&T), or
  • exactly one exclusive reference (&mut T),

but never both. And every reference must be valid: it can't outlive the value it points to.

A useful mental shorthand: aliasing XOR mutation. Data may be shared, or it may be changed, but not both at once. That single rule rules out data races, iterator invalidation, and a large family of "I changed it while someone else was reading it" bugs.

Here's the classic violation:

let mut names = vec![String::from("a"), String::from("b")];

let first = &names[0];         // shared borrow of `names`
names.push(String::from("c")); // error[E0502]: cannot borrow `names` as mutable
                               // because it is also borrowed as immutable
println!("{first}");

This isn't pedantry. push may reallocate the vector's buffer, which would leave first pointing at freed memory. In C++ this compiles and crashes later (sometimes); in Rust it doesn't compile.

Borrows end at their last use

A borrow lasts until the reference is last used, not until the end of the block. This is called non-lexical lifetimes (NLL), and it makes many apparent conflicts disappear:

let mut names = vec![String::from("a"), String::from("b")];

let first = &names[0];
println!("{first}");           // last use of `first`: the shared borrow ends here

names.push(String::from("c")); // fine: no outstanding borrows

When you hit a borrow error, a common fix is simply to reorder statements so the reference's last use comes before the conflicting operation, or to copy out the small piece of data you need (let first_len = names[0].len();) instead of holding a reference.

Copy and Clone: when the original stays usable

Some types are so small and simple that duplicating them is trivially cheap and can never cause a double free. These implement the Copy trait, and assignment copies them instead of moving them:

let a = 10;
let b = a;          // copy, not move
println!("{a} {b}"); // both usable

Copy types include all integers and floats, bool, char, shared references &T, and tuples/arrays made only of Copy types. A type that owns heap memory or any other resource (String, Vec<T>, Box<T>, File) can never be Copy.

Your own types can opt in with #[derive(Clone, Copy)] if all their fields are Copy. Do this for small, plain-data value types (a 2D point, an ID wrapper, a colour). Don't add it just to silence a move error. Ask whether copying the type makes semantic sense first.

For everything else there's Clone, an explicit, potentially expensive duplication:

let first = String::from("hello");
let second = first.clone(); // allocates a new buffer and copies the bytes

println!("{first} {second}");

clone() is the right tool when you genuinely need a second, independent owned value. It's the wrong tool as a reflex every time the borrow checker complains, because that usually hides a design question (who should own this?) behind an allocation.

Slices: borrowing part of a value

A slice is a reference to a contiguous part of something. The two you'll meet constantly:

Owned type Borrowed view Notes
String &str String literals ("hi") are &'static str.
Vec<T> &[T] Arrays [T; N] also coerce to &[T].
PathBuf &Path Filesystem paths.
OsString &OsStr Platform-native strings.
fn first_word(s: &str) -> &str {
    s.split_whitespace().next().unwrap_or("")
}

fn main() {
    let owned = String::from("hello world");
    println!("{}", first_word(&owned));     // &String coerces to &str
    println!("{}", first_word("literal"));  // works with literals too
}

Guideline: function parameters should usually take the borrowed form (&str, &[T], &Path) rather than &String or &Vec<T>. It's strictly more flexible for callers and costs nothing.

Lifetimes: how long is a reference valid?

A lifetime is the region of code during which a reference is valid. The compiler tracks lifetimes for every reference; most of the time it infers them silently. You only write them when a function's signature is ambiguous about how its inputs and outputs relate.

The compiler refuses to let a reference outlive its data:

let r;
{
    let x = 5;
    r = &x;      // error[E0597]: `x` does not live long enough
}
println!("{r}");

When you have to annotate

Consider a function that returns one of two string slices:

fn longer(a: &str, b: &str) -> &str { // error[E0106]: missing lifetime specifier
    if a.len() >= b.len() { a } else { b }
}

The returned reference points into either a or b, and the compiler can't tell which from the signature alone. You tell it with a lifetime parameter:

fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() >= b.len() { a } else { b }
}

Read 'a as: "the result is valid for as long as both inputs are valid." Lifetime annotations describe relationships; they never make anything live longer.

Elision: why you rarely write them

The compiler applies three elision rules so common signatures need no annotations:

  1. Each reference parameter gets its own lifetime.
  2. If there is exactly one input lifetime, it's assigned to all output references.
  3. If one of the parameters is &self or &mut self, its lifetime is assigned to all output references.

That's why fn first_word(s: &str) -> &str and fn name(&self) -> &str need nothing extra.

Structs that hold references

A struct that stores a reference must declare a lifetime, because the struct can't outlive what it borrows:

struct Parser<'src> {
    input: &'src str,
    pos: usize,
}

This is useful for short-lived, zero-copy views (parsers, iterators). For long-lived data, prefer owning fields (String, Vec<T>). Structs full of lifetime parameters are a frequent source of beginner pain, and owning the data is usually simpler and plenty fast.

'static

'static means "valid for the entire run of the program." String literals are &'static str. You'll also see the bound T: 'static, as in std::thread::spawn and tokio::spawn. That bound means "T contains no borrowed references that could expire," which is satisfied by any fully-owned type such as String or Vec<u8>. It does not mean the value lives forever.

When one owner isn't enough: smart pointers

Single ownership covers most code. When it genuinely doesn't (a graph, a cache shared between threads, application state shared by request handlers), the standard library provides types that implement other ownership policies:

Type Policy Typical use
Box<T> Single owner, value on the heap Recursive types, large values, trait objects (Box<dyn Trait>).
Rc<T> Shared ownership via reference counting, single thread Trees/graphs where several nodes point at one value.
Arc<T> Shared ownership, atomic reference count, thread-safe Sharing config or state across threads/async tasks.
RefCell<T> Borrow rules checked at runtime, single thread Interior mutability behind an Rc.
Mutex<T> / RwLock<T> Exclusive / reader-writer locking, thread-safe Mutable state behind an Arc.
Cow<'a, T> Borrowed until you need to modify, then owned Functions that usually return input unchanged.
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0));

    let handles: Vec<_> = (0..4)
        .map(|_| {
            let counter = Arc::clone(&counter); // new owner, same value
            thread::spawn(move || {
                *counter.lock().unwrap() += 1;
            })
        })
        .collect();

    for h in handles {
        h.join().unwrap();
    }
    println!("{}", *counter.lock().unwrap()); // 4
}

Rc<RefCell<T>> and Arc<Mutex<T>> are legitimate tools, but if they appear everywhere it's usually a sign the program's ownership structure needs rethinking. See Architecting Rust Projects for design alternatives.

Common compiler errors and what they mean

Code Message (abridged) What usually happened Usual fix
E0382 use of moved / partially moved value You moved a value (or a field of it), then used it again. Borrow instead (&x, ref, match &x), reorder, or clone() if a second owner is truly needed.
E0499 cannot borrow as mutable more than once Two live &mut to the same value. Shorten one borrow's scope; split the struct; use indices.
E0502 cannot borrow as mutable because it is also borrowed as immutable A & is still live when you take &mut. Finish using the & first, or copy out what you need.
E0505 cannot move out because it is borrowed Moving a value while a reference to it is live. End the borrow before the move.
E0507 cannot move out of a shared reference Taking ownership of something you only borrowed. Borrow the inner value, clone(), or std::mem::take/Option::take.
E0597 does not live long enough A reference outlives its data. Move the owner to an outer scope, or return owned data.
E0106 missing lifetime specifier A returned reference's source is ambiguous. Add a lifetime parameter, or return an owned value.
E0596 cannot borrow as mutable The binding isn't declared mut. let mut x, or take &mut self.

rustc --explain E0382 prints a full explanation with examples for any of these.

A practical checklist

When working with a value, ask:

  1. Who owns this? Exactly one place should be responsible for it.
  2. Does this code need ownership, or just access? Reading → &T. Modifying → &mut T. Keeping or consuming → T.
  3. Will the value be needed again after this line? If so, don't move it; borrow it.
  4. Will the reference outlive the data? If a reference needs to escape a function, return owned data instead.
  5. Is a clone a design decision or a band-aid? Clone deliberately, not reflexively.

Quick reference

Concept Plain-language meaning
Ownership Exactly one part of the program is responsible for a value.
Drop The value is cleaned up when its owner goes out of scope.
Move Ownership transfers; the old binding can't be used.
Borrow Temporary access via a reference; ownership stays put.
&T Shared reference: read-only, many allowed.
&mut T Exclusive reference: read-write, only one allowed.
NLL Borrows end at the reference's last use, not the end of the block.
Copy Implicit, cheap bitwise duplication; original stays usable.
Clone Explicit, possibly expensive duplication.
Slice A borrowed view into part of a collection (&str, &[T]).
Partial move A field was moved out, so the whole value can't be used.
Lifetime The region where a reference is valid; annotations describe relationships.
Rc / Arc Shared ownership via reference counting.
RefCell / Mutex Mutation through a shared handle, checked at runtime.