Rust Ownership
Ownership is the single most important concept in Rust. It is the rule set that allows Rust to guarantee memory safety and thread safety without relying on a garbage collector or a virtual machine. Every value in Rust has exactly one owner, and the compiler enforces when that value can be used, moved, borrowed, or dropped.
If you come from C or C++, ownership replaces the manual bookkeeping of malloc and free with compile-time checks. If you come from JavaScript, Python, or Go, ownership replaces the runtime garbage collector with deterministic destruction and explicit transfer of responsibility. The learning curve is real, but the payoff is predictable performance, zero-cost abstractions, and elimination of entire classes of bugs.
This guide walks through the ownership model from first principles: the three ownership rules, move semantics, borrowing, mutable references, slices, lifetimes, and the most common borrow-checker errors you will encounter. By the end, you will be able to read and write idiomatic Rust that uses the type system rather than fighting it.
info
Rust's ownership model is built on three rules that apply to every value on the heap and to most values on the stack. Internalizing these rules makes the rest of the language fall into place.
| Rule | Meaning | Consequence |
|---|---|---|
| One owner | Every value has a single variable that owns it | Ownership can be moved, never silently shared |
| One mutable borrow | At any moment, there is either one mutable reference or any number of immutable references | Prevents data races and iterator invalidation at compile time |
| References must be valid | A reference cannot outlive the data it points to | Dangling pointers and use-after-free are impossible |
The first rule is enforced by move semantics. When you assign a non-Copy value to a new variable or pass it to a function, ownership moves. The original variable becomes invalid and the compiler will reject any later use of it.
| 1 | fn main() { |
| 2 | let s1 = String::from("hello"); |
| 3 | let s2 = s1; // ownership moves from s1 to s2 |
| 4 | |
| 5 | // println!("{}", s1); // ERROR: value borrowed after move |
| 6 | println!("{}", s2); // OK: s2 is the owner |
| 7 | } |
warning
Rust distinguishes between cheap bitwise copies and expensive deep copies. A type implements Copy when duplicating it is trivial and deterministic: scalars, references, tuples of Copy types, and arrays of Copy types all qualify. Everything else must explicitly opt into duplication via Clone.
| 1 | fn main() { |
| 2 | let x: i32 = 42; |
| 3 | let y = x; // i32 is Copy, so both are valid |
| 4 | println!("x = {}, y = {}", x, y); |
| 5 | |
| 6 | let s1 = String::from("hello"); |
| 7 | let s2 = s1.clone(); // explicit deep copy |
| 8 | println!("s1 = {}, s2 = {}", s1, s2); |
| 9 | } |
You can implement Copy and Clone for your own types with a derive attribute, but only when every field is itself Copy or Clone. A struct containing a String cannot derive Copy because duplicating it would imply sharing or reallocating heap memory.
| 1 | #[derive(Clone, Copy)] |
| 2 | struct Point { |
| 3 | x: i32, |
| 4 | y: i32, |
| 5 | } |
| 6 | |
| 7 | fn main() { |
| 8 | let p1 = Point { x: 1, y: 2 }; |
| 9 | let p2 = p1; |
| 10 | println!("p1.x = {}, p2.x = {}", p1.x, p2.x); |
| 11 | } |
best practice
Passing a value to a function follows the same move rules as assignment. If the function needs to keep the value, it takes ownership. If it only needs to inspect the value, it borrows. If it needs to modify the value, it borrows mutably.
| 1 | fn takes_ownership(s: String) { |
| 2 | println!("{}", s); |
| 3 | } // s is dropped here |
| 4 | |
| 5 | fn borrows(s: &String) { |
| 6 | println!("{}", s); |
| 7 | } // s is not dropped; we do not own it |
| 8 | |
| 9 | fn mutates(s: &mut String) { |
| 10 | s.push_str(", world"); |
| 11 | } |
| 12 | |
| 13 | fn main() { |
| 14 | let a = String::from("hello"); |
| 15 | takes_ownership(a); |
| 16 | // println!("{}", a); // ERROR: a was moved |
| 17 | |
| 18 | let b = String::from("hello"); |
| 19 | borrows(&b); |
| 20 | println!("b is still: {}", b); // OK |
| 21 | |
| 22 | let mut c = String::from("hello"); |
| 23 | mutates(&mut c); |
| 24 | println!("c is now: {}", c); // OK |
| 25 | } |
Returning ownership is one way to let a caller use a value again after a function call, but it is often noisy. Borrowing is the idiomatic default: pass immutable references for read-only access and mutable references when the function must change the data.
| 1 | fn take_and_give_back(s: String) -> String { |
| 2 | println!("{}", s); |
| 3 | s |
| 4 | } |
| 5 | |
| 6 | fn main() { |
| 7 | let s = String::from("hello"); |
| 8 | let s = take_and_give_back(s); // ownership moves in, then out |
| 9 | println!("{}", s); |
| 10 | } |
Borrowing lets you use a value without taking ownership. An immutable reference, written &T, guarantees that the underlying data will not change for the lifetime of the reference. Multiple immutable references can coexist because readers do not interfere with each other.
| 1 | fn main() { |
| 2 | let s = String::from("hello"); |
| 3 | |
| 4 | let r1 = &s; |
| 5 | let r2 = &s; |
| 6 | let r3 = &s; |
| 7 | |
| 8 | println!("{} {} {}", r1, r2, r3); |
| 9 | } |
A mutable reference, written &mut T, grants exclusive access. While a mutable reference is alive, no other reference to the same data may exist. This rule is the foundation of Rust's freedom from data races: if a thread has a mutable reference, no other thread can be reading or writing the same data.
| 1 | fn main() { |
| 2 | let mut s = String::from("hello"); |
| 3 | |
| 4 | let r1 = &mut s; |
| 5 | // let r2 = &mut s; // ERROR: cannot borrow as mutable more than once |
| 6 | // let r3 = &s; // ERROR: cannot borrow as immutable while mutable borrow is active |
| 7 | |
| 8 | r1.push_str(", world"); |
| 9 | println!("{}", r1); |
| 10 | } |
note
The borrow checker collapses to two rules: you may have either one mutable reference or any number of immutable references, and they must be non-overlapping. The following table shows valid and invalid patterns.
| Pattern | Allowed? | Why |
|---|---|---|
| &a then &a | Yes | Multiple immutable readers are safe |
| &mut a then &mut a | No | Two writers could invalidate each other |
| &a then &mut a | No | A reader assumes data is stable |
| &mut a then &a | No | A writer could invalidate a new reader |
| &mut a; drop(r); &mut a | Yes | Non-overlapping lifetimes are fine |
The compiler tracks lifetimes at the statement level, so re-borrowing after the last use of a previous borrow is allowed. This often surprises newcomers who expect braces to define borrow boundaries.
| 1 | fn main() { |
| 2 | let mut s = String::from("hello"); |
| 3 | |
| 4 | let r1 = &s; |
| 5 | println!("{}", r1); // last use of r1 |
| 6 | |
| 7 | let r2 = &mut s; // OK: r1's lifetime has ended |
| 8 | r2.push_str("!"); |
| 9 | println!("{}", r2); |
| 10 | } |
A slice is a view into a contiguous sequence: a string slice &str references part of a string, and an array slice &[T] references part of an array or vector. Slices borrow from the underlying collection, so they carry the same lifetime guarantees as references.
| 1 | fn first_word(s: &str) -> &str { |
| 2 | let bytes = s.as_bytes(); |
| 3 | |
| 4 | for (i, &item) in bytes.iter().enumerate() { |
| 5 | if item == b' ' { |
| 6 | return &s[0..i]; |
| 7 | } |
| 8 | } |
| 9 | |
| 10 | &s[..] |
| 11 | } |
| 12 | |
| 13 | fn main() { |
| 14 | let s = String::from("hello world"); |
| 15 | let word = first_word(&s); |
| 16 | // s.clear(); // ERROR: cannot mutate while borrowed |
| 17 | println!("first word: {}", word); |
| 18 | } |
String literals have type &'static str: they are slices into the read-only data segment of the binary and live for the entire program. Function parameters should usually prefer &str over &String, because &str accepts both string slices and references to owned strings.
| 1 | fn main() { |
| 2 | let a = [1, 2, 3, 4, 5]; |
| 3 | let slice = &a[1..3]; // [2, 3] |
| 4 | println!("{:?}", slice); |
| 5 | |
| 6 | let v = vec![10, 20, 30, 40]; |
| 7 | let mid = &v[1..=2]; // [20, 30] |
| 8 | println!("{:?}", mid); |
| 9 | } |
best practice
A lifetime is a named region of code during which a reference is valid. The compiler infers most lifetimes automatically, but you must write them explicitly when a function returns a reference and the compiler cannot prove which input the reference outlives.
| 1 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { |
| 2 | if x.len() > y.len() { |
| 3 | x |
| 4 | } else { |
| 5 | y |
| 6 | } |
| 7 | } |
| 8 | |
| 9 | fn main() { |
| 10 | let s1 = String::from("short"); |
| 11 | let result; |
| 12 | { |
| 13 | let s2 = String::from("longer string"); |
| 14 | result = longest(&s1, &s2); |
| 15 | println!("longest: {}", result); |
| 16 | } // s2 dropped here; result's lifetime ends here too |
| 17 | } |
The signature fn longest<a>(x: &'a str, y: &'a str) -> &'a str says: the returned reference lives at least as long as the shorter of the two input references. The caller must keep both inputs alive for as long as the result is used.
| 1 | struct Excerpt<'a> { |
| 2 | part: &'a str, |
| 3 | } |
| 4 | |
| 5 | fn main() { |
| 6 | let novel = String::from("Call me Ishmael. Some years ago..."); |
| 7 | let first_sentence = novel.split('.').next().unwrap(); |
| 8 | let excerpt = Excerpt { |
| 9 | part: first_sentence, |
| 10 | }; |
| 11 | println!("excerpt: {}", excerpt.part); |
| 12 | } |
warning
Rust has three lifetime elision rules that let you omit annotations in common cases. If the compiler can apply the rules unambiguously, you do not need to write the lifetimes yourself.
| Rule | Applies to | Behavior |
|---|---|---|
| 1 | Each input reference | Gets its own lifetime parameter |
| 2 | Single input reference | Output lifetime equals input lifetime |
| 3 | &self or &mut self method | Output lifetime equals self lifetime |
The classic example is fn first_word(s: &str) -> &str. Rule 1 gives the input a lifetime, rule 2 says the output shares that lifetime, so the function compiles without annotations.
| 1 | struct Book<'a> { |
| 2 | title: &'a str, |
| 3 | } |
| 4 | |
| 5 | impl<'a> Book<'a> { |
| 6 | fn title(&self) -> &str { |
| 7 | self.title |
| 8 | } |
| 9 | } |
| 10 | |
| 11 | fn main() { |
| 12 | let title = String::from("The Rust Programming Language"); |
| 13 | let book = Book { title: &title }; |
| 14 | println!("{}", book.title()); |
| 15 | } |
The borrow checker errors are descriptive but dense. Recognizing the patterns below will help you fix them quickly without resorting to unnecessary cloning.
| Error | Cause | Fix |
|---|---|---|
| use of moved value | Using a value after ownership moved | Borrow instead, clone if duplication is intended, or restructure ownership |
| cannot borrow as mutable more than once | Two overlapping &mut borrows | Limit the borrow's scope, split borrows, or use interior mutability |
| cannot borrow as mutable because it is also borrowed as immutable | & and &mut overlap | End the immutable borrow before the mutable one |
| borrowed value does not live long enough | Reference outlives its referent | Return owned data, change lifetime annotations, or keep data alive longer |
| dropped here while still borrowed | Referent dropped before reference | Drop the reference first or extend the owner's lifetime |
| 1 | fn main() { |
| 2 | let s = String::from("hello"); |
| 3 | let r = &s; |
| 4 | let len = r.len(); |
| 5 | |
| 6 | // s.push_str(", world"); // ERROR: cannot borrow mutably while immutable borrow active |
| 7 | println!("{} has length {}", r, len); // last use of r |
| 8 | |
| 9 | // Now the immutable borrow has ended |
| 10 | let mut s = s; // shadow with mutable binding |
| 11 | s.push_str(", world"); |
| 12 | println!("{}", s); |
| 13 | } |
danger
Idiomatic Rust leans on ownership to express resource management. RAII, scope guards, builder patterns, and interior mutability each solve a specific class of problem while staying within the ownership rules.
| 1 | use std::fs::File; |
| 2 | use std::io::{self, Write}; |
| 3 | |
| 4 | fn write_log() -> io::Result<()> { |
| 5 | let mut file = File::create("app.log")?; |
| 6 | file.write_all(b"application started |
| 7 | ")?; |
| 8 | Ok(()) // file is closed automatically when it goes out of scope |
| 9 | } |
| 10 | |
| 11 | fn main() { |
| 12 | if let Err(e) = write_log() { |
| 13 | eprintln!("failed to write log: {}", e); |
| 14 | } |
| 15 | } |
RAII means that resource acquisition is initialization: the moment a value is created, it owns its resources, and the moment it goes out of scope, its destructor runs. Files, locks, network sockets, and database connections all benefit from deterministic cleanup.
| 1 | fn split_borrow(v: &mut [i32]) { |
| 2 | let (left, right) = v.split_at_mut(2); |
| 3 | left[0] += 1; |
| 4 | right[0] += 10; |
| 5 | } |
| 6 | |
| 7 | fn main() { |
| 8 | let mut values = [1, 2, 3, 4]; |
| 9 | split_borrow(&mut values); |
| 10 | println!("{:?}", values); // [2, 2, 13, 4] |
| 11 | } |
When you need multiple mutable references to different parts of a collection, use borrowing APIs that split the collection into disjoint slices. This satisfies the borrow checker because the returned slices are known not to overlap.
| 1 | use std::cell::RefCell; |
| 2 | |
| 3 | fn main() { |
| 4 | let data = RefCell::new(vec![1, 2, 3]); |
| 5 | |
| 6 | { |
| 7 | let mut borrow = data.borrow_mut(); |
| 8 | borrow.push(4); |
| 9 | } |
| 10 | |
| 11 | println!("{:?}", data.borrow()); |
| 12 | } |
note
Smart pointers extend ownership with runtime behavior. Box<T> owns heap-allocated data, Rc<T> enables shared ownership with reference counting, and Arc<T> does the same across threads. Cow<T> lets code choose between borrowing and cloning lazily.
| 1 | use std::rc::Rc; |
| 2 | |
| 3 | fn main() { |
| 4 | let data = Rc::new(String::from("shared")); |
| 5 | |
| 6 | let a = Rc::clone(&data); |
| 7 | let b = Rc::clone(&data); |
| 8 | |
| 9 | println!("count: {}", Rc::strong_count(&data)); |
| 10 | println!("{} {} {}", data, a, b); |
| 11 | } |
Rc is for single-threaded shared ownership. For multi-threaded code, use Arc and pair it with a synchronization primitive such as Mutex or RwLock if you need mutation.
| 1 | use std::sync::{Arc, Mutex}; |
| 2 | use std::thread; |
| 3 | |
| 4 | fn main() { |
| 5 | let counter = Arc::new(Mutex::new(0)); |
| 6 | let mut handles = vec![]; |
| 7 | |
| 8 | for _ in 0..10 { |
| 9 | let c = Arc::clone(&counter); |
| 10 | let handle = thread::spawn(move || { |
| 11 | let mut n = c.lock().unwrap(); |
| 12 | *n += 1; |
| 13 | }); |
| 14 | handles.push(handle); |
| 15 | } |
| 16 | |
| 17 | for h in handles { |
| 18 | h.join().unwrap(); |
| 19 | } |
| 20 | |
| 21 | println!("result: {}", *counter.lock().unwrap()); |
| 22 | } |
Experienced Rust code tends to follow a small set of ownership idioms. Learning them early prevents the most common frustrations.
| Idiom | When to use |
|---|---|
| Pass &T for reads | Default for function arguments that inspect data |
| Pass &mut T for writes | When the function must modify the caller's data |
| Pass T to consume | When the function becomes the sole owner |
| Return T to transfer back | When ownership must return to the caller |
| Prefer slices over &Vec and &String | Accepts literals, owned values, and references |
| Use Copy for small values | Cheap, immutable types reduce move friction |
| 1 | fn process_lines(lines: &[&str]) -> Vec<String> { |
| 2 | lines |
| 3 | .iter() |
| 4 | .map(|line| line.trim().to_string()) |
| 5 | .collect() |
| 6 | } |
| 7 | |
| 8 | fn main() { |
| 9 | let raw: Vec<&str> = vec![" hello ", "world"]; |
| 10 | let cleaned = process_lines(&raw); |
| 11 | println!("{:?}", cleaned); |
| 12 | } |
best practice
The following example ties together ownership, borrowing, lifetimes, and slices in a small but realistic program. It parses a command string, tokenizes it, and keeps the tokens as references into the original input rather than allocating new strings.
| 1 | fn tokenize(input: &str) -> Vec<&str> { |
| 2 | input.split_whitespace().collect() |
| 3 | } |
| 4 | |
| 5 | fn longest_token<'a>(tokens: &[&'a str]) -> Option<&'a str> { |
| 6 | tokens.iter().max_by_key(|t| t.len()).copied() |
| 7 | } |
| 8 | |
| 9 | fn main() { |
| 10 | let command = String::from("cargo build --release --target wasm32"); |
| 11 | let tokens = tokenize(&command); |
| 12 | |
| 13 | match longest_token(&tokens) { |
| 14 | Some(token) => println!("longest token: {}", token), |
| 15 | None => println!("no tokens"), |
| 16 | } |
| 17 | |
| 18 | println!("all tokens: {:?}", tokens); |
| 19 | } |
Because tokens borrows from command, the compiler guarantees that command outlives tokens. The helper functions accept slices and string slices, so they work with owned strings, string literals, and borrowed data without modification.
Community
Get help on Slack, Discord or VIP
Stuck on a guide? Join the community and ask.