Ownership makes one variable or place responsible for each Rust value; when the owner leaves scope, the value is destroyed according to its rules.
Assigning or passing a non-Copy value by value normally moves ownership, so the old binding can no longer be used. Adding clone() may compile, but can change cost and resource semantics.
Decide whether a function must take, read, or mutate a value, then use T, &T, or &mut T respectively. Call clone() only when the contract needs an independent copy, and inspect every destruction path.
What it is and why it exists
Ownership is Rust’s set of language rules for stating who is responsible for a value, when that responsibility transfers, and when the value ends. A value may own more than heap memory: it may own a file, socket, lock guard, or another resource that needs cleanup. The compiler checks the rules at compile time, while a type’s destruction logic runs as the corresponding scope exits at runtime.
Each value has a current owner, and two variables cannot independently own the same ordinary owned value at once. Assignment, by-value arguments, and by-value returns can transfer responsibility from one place to another; this is move semantics . Rejecting the old binding after a move leaves one owning path responsible for releasing the resource.
When an owner leaves scope, Rust runs the value’s destruction logic and then releases the resources it owns. This pattern is Resource Acquisition Is Initialization (RAII) : a resource’s lifetime is tied to its owning value instead of waiting for a garbage collector to find an unreachable object. Control flow and scope usually determine the release point directly.
Not every assignment invalidates the source. Types that implement Copy are copied implicitly on assignment and by-value calls; examples include integers, booleans, and many structs whose fields are all Copy. A String owns a heap buffer and does not implement Copy, so assignment moves it by default; call clone() explicitly when you need another copy of the string data.
Borrowing lets a function access a value temporarily without taking ownership. &T is a shared borrow and &mut T is an exclusive mutable borrow. Ownership answers “who is ultimately responsible for ending this value,” while borrowing answers “who may access this value for some period.”
You encounter ownership in function signatures, collection iteration, pattern matching, closure capture, and thread boundaries. It is a resource-responsibility model, not a performance rule that says “never copy.” Express responsibility accurately first, then use analysis and measurement to decide whether copies or allocations should be reduced.
How it works
Separate bindings, values, and resources when reasoning about ownership. A variable name binds to a value, and that value may own a resource elsewhere; for example, a String value manages a growable UTF-8 buffer. Moving a String transfers responsibility for managing the buffer without creating a second copy of its text.
Rust teaching material commonly summarizes the basic rules as follows:
- Each value has an owner.
- There can be only one owner at a time.
- The value is dropped when its owner leaves scope.
These rules describe single ownership by default. Types such as Rc<T> and Arc<T> model several owning handles through their own APIs, but the responsibility remains explicit: the last strong owner ends the value. The details of counts and thread behavior belong to the smart-pointer topic.
A move is a static state change
After let next = current; for a non-Copy value, next is the owner. The compiler treats current as moved instead of waiting for a runtime check before each use. An old variable can be assigned and used again when every control-flow path proves that it has received a new value.
A by-value argument also moves a value because the callee’s parameter is a new owning place. A by-value return moves the returned value to the caller. The compiler eliminates many physical shuffles used only to express these semantics, so a language-level move does not mean “copy the entire heap object.”
A move can affect only one field of a struct. Unmoved fields may still be used separately afterward, but the whole struct normally cannot be used as a complete value. Types that implement Drop are more restricted because their destructor must receive a complete, valid self.
Copy and Clone are different contracts
Copy is a marker trait with no methods. Once a type implements it, ordinary assignment leaves the source binding available, and that copy cannot run custom code. A type that implements Drop cannot also implement Copy, because implicit duplication would make destruction responsibility ambiguous.
Clone provides an explicit clone() method and lets a type define how it creates a logical copy. Cloning a String copies its text data; cloning an Rc<T> only increments the shared-owner count; cloning a custom type follows its implementation. For every .clone(), inspect the receiver type to determine cost and ownership meaning.
Copy is not a synonym for “stored on the stack.” Shared references and some values containing pointers can be Copy, while a fixed-size stack value may own resources or implement Drop and therefore cannot be Copy. The type contract decides, not a guessed storage location.
| Operation | For non-Copy T | For Copy T | Ownership meaning |
|---|---|---|---|
let b = a; | Moves; a is invalid | Copies; a stays valid | b receives a value |
consume(a) | Moves ownership into the function | Copies the argument value | Parameter is by value |
inspect(&a) | Shared borrow | Shared borrow | Caller keeps ownership |
a.clone() | Explicitly creates a copy | Explicitly creates a copy | Semantics come from Clone |
drop(a) | Immediately consumes and destroys | Consumes a copied argument value | drop merely takes by value |
Borrowing preserves the ownership boundary
A function that only reads a value during its call usually accepts &T. One that mutates without retaining the value usually accepts &mut T. Taking T by value accurately signals a responsibility change when the function must store, transfer, or eventually destroy the value.
A borrow has its own valid region. A reference cannot outlive the owner, and conflicting shared and mutable access cannot overlap. The compiler normally ends a borrow after the reference’s last use instead of mechanically extending it to a closing brace. The related topics cover conflict rules and lifetime annotations in detail.
Drop completes the handoff
Rust automatically calls Drop::drop as a scope exits; callers cannot invoke that method directly. To end a value early, the standard drop(value) function takes it by value and lets destruction finish before the function returns. Calling drop(&value) only ends that copyable reference, not the referenced value.
Local variables are generally dropped in reverse declaration order, while struct fields are dropped in declaration order. Early return and an unwinding panic also drop initialized local values that remain alive. Paths such as std::process::exit, process abort, and strong reference-counting cycles can bypass expected destruction, so ownership guarantees memory safety rather than guaranteeing that every destructor runs.
Examples
The following four programs demonstrate by-value transfer, Copy and Clone, borrowing, and deterministic destruction. Each was executed locally with Rust 1.98.0 using cargo run, and the output blocks contain the real results.
Moving ownership into and out of a function
approve takes a Ticket by value, so ownership moves from main into the call. Its return transfers ownership back to the caller; binding the result under the same name makes the state transition read as one continuous flow.
#[derive(Debug)]
struct Ticket {
id: u32,
status: String,
}
fn approve(mut ticket: Ticket) -> Ticket {
ticket.status = String::from("approved");
ticket
}
fn main() {
let ticket = Ticket {
id: 42,
status: String::from("pending"),
};
let ticket = approve(ticket);
println!("ticket {}: {}", ticket.id, ticket.status);
}ticket 42: approvedThe first ticket has been moved after the call, and the second binding with the same name owns the returned value. This shadowing does not restore the old value; it gives a suitable domain name to the new owner. If approve did not need to retain or return the ticket, accepting &mut Ticket would let the caller keep ownership throughout.
Comparing an implicit copy with an explicit clone
The integer implements Copy, so retry_limit remains usable after assignment. Shipment contains a String and cannot implement Copy; its derived Clone implementation explicitly creates an independent shipment record.
#[derive(Clone, Debug)]
struct Shipment {
route: String,
priority: u8,
}
fn main() {
let retry_limit = 3;
let copied_limit = retry_limit;
let original = Shipment {
route: String::from("Paris-Lyon"),
priority: 1,
};
let mut rerouted = original.clone();
rerouted.route = String::from("Paris-Dijon");
rerouted.priority = 2;
println!("limits: {retry_limit}, {copied_limit}");
println!("original: {original:?}");
println!("rerouted: {rerouted:?}");
}limits: 3, 3
original: Shipment { route: "Paris-Lyon", priority: 1 }
rerouted: Shipment { route: "Paris-Dijon", priority: 2 }Changing the clone does not change the original because the two String values own separate buffers. That result follows from the derived Clone implementation cloning each Shipment field; it does not mean every .clone() performs a deep copy. A shared pointer usually creates another owning handle when cloned.
Borrowing instead of transferring
note_count only reads an order, so it accepts &Order; add_note must mutate it, so it accepts &mut Order. Neither function retains the order, and ownership stays in main throughout.
#[derive(Debug)]
struct Order {
number: String,
notes: Vec<String>,
}
fn note_count(order: &Order) -> usize {
order.notes.len()
}
fn add_note(order: &mut Order, note: &str) {
order.notes.push(note.to_owned());
}
fn main() {
let mut order = Order {
number: String::from("A-17"),
notes: vec![String::from("paid")],
};
println!("{} has {} note", order.number, note_count(&order));
add_note(&mut order, "packed");
println!("{} has {} notes", order.number, note_count(&order));
}A-17 has 1 note
A-17 has 2 notesThe first shared borrow is no longer used after its println!, so the later mutable borrow is valid. note is a borrowed &str, but the order must retain the text after the function returns, so to_owned() creates a String at the real retention boundary. That allocation belongs to the interface semantics; it is not there merely to appease the compiler.
Observing scopes and explicit drop
The Traced destructor prints a name, making drop order visible. The inner _buffer is dropped automatically when its block ends, while connection ends early through drop.
struct Traced(&'static str);
impl Drop for Traced {
fn drop(&mut self) {
println!("drop {}", self.0);
}
}
fn main() {
let connection = Traced("connection");
{
let _buffer = Traced("buffer");
println!("inside scope");
}
println!("after inner scope");
drop(connection);
println!("after explicit drop");
}inside scope
drop buffer
after inner scope
drop connection
after explicit dropdrop(connection) consumes the value, so connection cannot be used afterward. Destructors on real types normally release resources without printing; the output only exposes the timing. To release a lock guard, drop the guard itself rather than a reference to it.
Pitfalls
Using an old binding after a move
Fix: Check whether the callee must retain or destroy the value. Use &T for reading, &mut T for in-place mutation, stop using the old binding after a real transfer, or return ownership from the function. Clone only when the contract requires two independent values.
Cloning away every ownership error
Fix: For each clone, state who owns the new copy and why it must be independent. Try shortening a borrow, moving the value, or changing the parameter type first. Justify a retained clone using the type’s semantics and measurement when needed, not a blanket claim that cloning is cheap.
Guessing Copy from storage location
Fix: Check whether the type implements Copy and whether all its fields permit that implementation. Derive Copy for a custom type only when implicit duplication matches the domain semantics. Use Clone or keep move semantics when copying needs explicit confirmation, allocation, or count updates.
Missing a partial move
Fix: Use ref in a pattern or match a reference when you only need to read. When a field must be extracted, destructure the complete value or model the field as Option<T> and call take() to leave a valid state. Do not use unsafe code to bypass destructor invariants.
Treating destruction as inevitable
Fix: Give files, locks, and transactions the smallest practical lexical scope, and do not put a required durable commit only in Drop. Mark owning edges in reference graphs and use Weak for non-owning back edges. Commit or flush explicitly during normal shutdown, leaving destruction as protection for exceptional paths.
Partial moves and Drop
Struct fields own their values separately, so a non-Copy field can be moved on its own. The compiler then tracks which fields remain initialized; you can read unmoved fields individually, but cannot borrow or move the whole struct again. This analysis happens at compile time and adds no runtime “partly valid” flag to the struct.
A pattern can use ref to borrow a field instead of moving it. Matching &value can likewise make the pattern operate on borrowed contents. Choose according to the later responsibility: borrow for observation, or explicitly leave a state that satisfies the type’s invariants when extracting a field.
A type that implements Drop cannot safely have arbitrary fields moved out because the compiler must still pass a complete &mut self to its destructor. A common design stores the field in Option<T> and uses take() to replace it with None before obtaining the original value. The destructor then still sees a valid state allowed by the type.
ManuallyDrop<T> and unsafe pointers can change automatic destruction behavior, but they also transfer responsibility for avoiding double drops and missed drops to the implementation. Ordinary application code should not use them to escape one move error. Reshape the ownership structure or expose a method that consumes the whole value first.
Ownership does not guarantee leak freedom
Rust’s safety guarantee concerns invalid memory access, not whether every allocation is eventually reclaimed. A program can deliberately call mem::forget or create a cycle of strong Rc or Arc edges; these cases can leak without creating a dangling reference. A leak consumes memory or retains resources such as files, but need not trigger undefined behavior.
In a reference-counted graph, a strong edge expresses ownership that keeps the target alive, while a weak edge provides optional access only. When a parent owns its children and a child merely navigates to its parent, the back edge usually uses Weak. If the domain permits general cycles, consider a central owner, an index-based arena, or an explicit cycle-breaking protocol instead of assuming reference counting detects cycles.
Destruction should not carry a business commit that must succeed. Drop::drop cannot return an ordinary error to its caller, and a second panic during panic unwinding can abort the process. Database commits, file flushes, and network acknowledgments need explicit methods returning Result; destruction should perform only infallible or best-effort cleanup.
API signatures are ownership contracts
Accepting T means the function receives a complete value and may store, transfer, or drop it. The signature does not promise that the function retains the value for long, but callers must program as if responsibility has transferred. Consuming builders and thread entry points commonly use this form.
Accepting &T means shared access within the borrow’s valid region and usually fits read-only queries. Accepting &mut T means exclusive access with permission to mutate, but the function still does not own T. If referenced data must be kept after the call, it needs an output lifetime relationship or an owned result created at that boundary.
Returning T gives a new or received value to the caller. Returning &T instead requires the result to trace to a still-valid input, field, or static value; a lifetime annotation only describes that relationship and does not extend the owner. When uncertain, write one sentence saying who must eventually drop the result. The answer usually determines whether the return should be owned or borrowed.
A conversion parameter such as impl Into<String> lets callers supply several representations, but it still creates an owned String inside the function. That convenience fits a deliberate retention boundary and should not hide that a read-only function could accept &str. Generics do not erase allocation or move behavior; they delegate the concrete conversion to the caller’s type.
When reviewing an API, label each parameter take, read, or mutate, and label each result new owner or borrowed from. Then compare the implementation with the signature: a read-only function should not demand ownership, a retaining function cannot store a short borrow, and a borrowed return cannot refer to a local temporary. This small table usually exposes a design mistake faster than adding clones after compiler errors.
Reading ownership diagnostics
An ownership error often appears at a later use even though its cause is an earlier move or borrow. The compiler marks both the first responsibility change and the invalid use. Read from the first transfer instead of editing only the final highlighted line.
“value moved here” means an expression took a non-Copy value by value. For a function call, inspect the parameter type; for a method call, also check whether the receiver is self, &self, or &mut self. A macro may hide the real call, so inspect its expansion or underlying API signature when necessary.
“cannot move out of” often means the code has borrowed access but tries to take an inner value. Returning a String field directly from &Record, for example, would move that field. Return &str when the interface only exposes a view; clone the field or redesign the ownership boundary only when the caller truly needs an owned result.
Trace a repair in this order:
- Find the last point where the value is definitely initialized.
- Mark the first move, shared borrow, or mutable borrow.
- Decide whether the failing use needs ownership, read access, or mutation access.
- Change the ownership relationship, signature, or operation order to match that need.
- Recheck every clone and explicit
dropto ensure none merely conceals the original problem.
A diagnostic suggestion is a locally possible operation, not necessarily the right API design. The compiler may suggest borrowing or cloning, but it does not know whether a value must be retained across a thread, represents a unique token, or loses its business identity when copied. The final repair must satisfy both the type rules and the domain contract.
| Diagnostic clue | First place to inspect | Common design problem |
|---|---|---|
use of moved value | Earlier assignment or by-value call | Caller assumes it still owns the value |
borrow of moved value | Move site and later borrow | Parameter takes T unnecessarily |
cannot move out of | Whether current access comes from &T or &mut T | Trying to take an owned field from a borrowed container |
cannot move out of type ... which implements Drop | Type’s destruction invariants | Field extraction leaves no valid state |
One edit can remove the first error while pushing the responsibility problem up one layer. Changing a parameter from String to &String, for example, preserves caller ownership, but a public read-only interface should usually go further and accept &str. After compilation succeeds, check whether the signature still imposes an unnecessary concrete container on callers.
Destruction boundaries and control flow
A closing brace is not the only way to leave a scope. Explicit return, break, and the ? operator can all leave a region containing owned values early, and Rust drops initialized values that cease to be alive on that path. Lock guards and temporary files can therefore use lexical scope for consistent cleanup across several return paths.
A conditional branch drops only values actually initialized on the executed path. Definite-initialization analysis prevents reads of an uninitialized binding and avoids running destruction for a value that never existed. A local created in a loop iteration is normally dropped at the end of that iteration unless ownership moves into a collection or result outside the loop.
When panic uses stack unwinding, Rust drops constructed local values while walking back through the stack. If the build chooses panic=abort, the process stops without unwind cleanup. Library code should not assume which panic strategy its caller selected for a durable operation that must succeed.
The table distinguishes ordinary control flow from paths that do not guarantee stack destruction:
| Path | Ordinary local destruction in the current stack | Design meaning |
|---|---|---|
| Reaching the end of a scope | Yes | Normal RAII path |
Early return or ? | Yes | Suitable for guards and temporary resources |
| Unwinding panic | Yes | Destructors must not panic again |
panic=abort or std::process::abort | No | Do not depend on in-process cleanup |
std::process::exit | No | Complete required shutdown explicitly first |
| Strong reference cycle retaining counts | Values in the cycle do not end | Repair the ownership graph or break the cycle |
An explicit drop(value) can shorten a resource lifetime, but a narrow braced scope is often easier to review. The scope also limits variable visibility, preventing later code from using a released or intentionally inaccessible resource. Use drop as a timing marker only when it makes the control flow clearer.
Field drop order can occasionally influence type design. If one field’s destructor accesses infrastructure managed by another field, declaration order becomes a hidden coupling. A more robust design performs required ordered, infallible cleanup explicitly in the owning type’s Drop implementation and keeps individual field destructors independent.
Temporary destruction points follow expression and statement rules and cannot always be inferred from visual nesting alone. Guards created in method chains, match scrutinees, and if let expressions deserve particular attention. When timing affects concurrency correctness, use named bindings and explicit scopes so the release point is visible in review.
Tests for destruction should prefer observable resource state over assertions about printed order alone. In controlled tests, a counter or weak pointer can confirm that the last owner disappeared, and early-return paths should also be covered. Abort behavior needs a child-process test because the test runner cannot continue in the same terminated process.
Further reading
4 questions · 2 predict-the-output · 1 spot-the-bug