|$ curl https://forge-ai.dev/api/markdown?path=docs/rust/ownership
$cat docs/rust-ownership.md
updated Recently·28 min read·published

Rust Ownership

RustSystems ProgrammingIntermediate🎯Free Tools
Introduction

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

Think of ownership as a contract between scopes. Each scope negotiates whether it owns a value, borrows it immutably, or borrows it mutably. The compiler is the arbiter that refuses any contract that could lead to a use-after-free, double free, data race, or dangling pointer.
The Three Ownership Rules

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.

RuleMeaningConsequence
One ownerEvery value has a single variable that owns itOwnership can be moved, never silently shared
One mutable borrowAt any moment, there is either one mutable reference or any number of immutable referencesPrevents data races and iterator invalidation at compile time
References must be validA reference cannot outlive the data it points toDangling 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.

move-basic.rs
RUST
1fn 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

Move semantics apply to heap-allocated types like String, Vec, HashMap, and custom types that do not implement Copy. Primitive types such as i32, f64, bool, and char implement Copy, so they are copied rather than moved.
Copy vs Clone

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.

copy-clone.rs
RUST
1fn 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.

derive-copy.rs
RUST
1#[derive(Clone, Copy)]
2struct Point {
3 x: i32,
4 y: i32,
5}
6
7fn 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

Derive Copy for small, immutable value types. Derive Clone for any type where duplication is meaningful. Never call .clone() to silence the borrow checker without first considering whether a reference or a move would be clearer.
Ownership and Functions

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.

ownership-functions.rs
RUST
1fn takes_ownership(s: String) {
2 println!("{}", s);
3} // s is dropped here
4
5fn borrows(s: &String) {
6 println!("{}", s);
7} // s is not dropped; we do not own it
8
9fn mutates(s: &mut String) {
10 s.push_str(", world");
11}
12
13fn 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.

return-ownership.rs
RUST
1fn take_and_give_back(s: String) -> String {
2 println!("{}", s);
3 s
4}
5
6fn 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

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.

multiple-borrows.rs
RUST
1fn 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.

mutable-borrow.rs
RUST
1fn 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 exclusivity rule applies to the borrow region, not just the line where the borrow begins. The compiler uses non-lexical lifetimes (NLT) to end a borrow as soon as the last use, so a mutable borrow often ends earlier than the closing brace of its scope.
The Borrowing Rules in Practice

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.

PatternAllowed?Why
&a then &aYesMultiple immutable readers are safe
&mut a then &mut aNoTwo writers could invalidate each other
&a then &mut aNoA reader assumes data is stable
&mut a then &aNoA writer could invalidate a new reader
&mut a; drop(r); &mut aYesNon-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.

non-lexical-lifetime.rs
RUST
1fn 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}
Slices

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.

string-slice.rs
RUST
1fn 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
13fn 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.

array-slice.rs
RUST
1fn 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

Prefer &str and &[T] in function signatures. Slices make APIs more flexible because they accept owned collections, borrowed references, and literal values without forcing callers to allocate.
Lifetimes

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.

lifetime-function.rs
RUST
1fn 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
9fn 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.

lifetime-struct.rs
RUST
1struct Excerpt<'a> {
2 part: &'a str,
3}
4
5fn 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

Lifetimes do not change how long values live; they only describe the relationship between references. A lifetime annotation is a promise that the compiler checks, not an instruction that extends or shortens a value's scope.
Lifetime Elision Rules

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.

RuleApplies toBehavior
1Each input referenceGets its own lifetime parameter
2Single input referenceOutput lifetime equals input lifetime
3&self or &mut self methodOutput 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.

lifetime-method.rs
RUST
1struct Book<'a> {
2 title: &'a str,
3}
4
5impl<'a> Book<'a> {
6 fn title(&self) -> &str {
7 self.title
8 }
9}
10
11fn main() {
12 let title = String::from("The Rust Programming Language");
13 let book = Book { title: &title };
14 println!("{}", book.title());
15}
Common Compile Errors

The borrow checker errors are descriptive but dense. Recognizing the patterns below will help you fix them quickly without resorting to unnecessary cloning.

ErrorCauseFix
use of moved valueUsing a value after ownership movedBorrow instead, clone if duplication is intended, or restructure ownership
cannot borrow as mutable more than onceTwo overlapping &mut borrowsLimit the borrow's scope, split borrows, or use interior mutability
cannot borrow as mutable because it is also borrowed as immutable& and &mut overlapEnd the immutable borrow before the mutable one
borrowed value does not live long enoughReference outlives its referentReturn owned data, change lifetime annotations, or keep data alive longer
dropped here while still borrowedReferent dropped before referenceDrop the reference first or extend the owner's lifetime
error-reborrow.rs
RUST
1fn 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

Do not silence borrow-checker errors with .clone() until you understand why the error occurred. Cloning is sometimes correct, but it can hide design issues and add unnecessary allocations.
Ownership Patterns

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.

raii.rs
RUST
1use std::fs::File;
2use std::io::{self, Write};
3
4fn 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
11fn 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.

split-borrow.rs
RUST
1fn split_borrow(v: &mut [i32]) {
2 let (left, right) = v.split_at_mut(2);
3 left[0] += 1;
4 right[0] += 10;
5}
6
7fn 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.

interior-mutability.rs
RUST
1use std::cell::RefCell;
2
3fn 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

Interior mutability types such as RefCell, Cell, and Mutex move some borrow checking from compile time to runtime. They are safe when used correctly, but RefCell will panic on a conflicting borrow rather than produce a compile error.
Smart Pointers and Ownership

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.

rc.rs
RUST
1use std::rc::Rc;
2
3fn 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.

arc-mutex.rs
RUST
1use std::sync::{Arc, Mutex};
2use std::thread;
3
4fn 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}
Idioms and Best Practices

Experienced Rust code tends to follow a small set of ownership idioms. Learning them early prevents the most common frustrations.

IdiomWhen to use
Pass &T for readsDefault for function arguments that inspect data
Pass &mut T for writesWhen the function must modify the caller's data
Pass T to consumeWhen the function becomes the sole owner
Return T to transfer backWhen ownership must return to the caller
Prefer slices over &Vec and &StringAccepts literals, owned values, and references
Use Copy for small valuesCheap, immutable types reduce move friction
slice-idiom.rs
RUST
1fn process_lines(lines: &[&str]) -> Vec<String> {
2 lines
3 .iter()
4 .map(|line| line.trim().to_string())
5 .collect()
6}
7
8fn main() {
9 let raw: Vec<&str> = vec![" hello ", "world"];
10 let cleaned = process_lines(&raw);
11 println!("{:?}", cleaned);
12}

best practice

Design functions so that the caller decides whether to move, borrow, or clone. A function that takes &str or &[T] is more reusable than one that requires an owned String or Vec.
Putting It Together

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.

ownership-example.rs
RUST
1fn tokenize(input: &str) -> Vec<&str> {
2 input.split_whitespace().collect()
3}
4
5fn longest_token<'a>(tokens: &[&'a str]) -> Option<&'a str> {
6 tokens.iter().max_by_key(|t| t.len()).copied()
7}
8
9fn 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.

$Blueprint — Engineering Documentation·Section ID: RUST-OWN·Revision: 1.0

Community

Get help on Slack, Discord or VIP

Stuck on a guide? Join the community and ask.