Rust ownership and borrowing
last modified June 7, 2026
In this article we explain ownership, borrowing, and slices in Rust.
Ownership is what makes Rust unique among mainstream programming languages. Most languages fall into one of two camps: either they use a garbage collector that periodically frees unused memory (Go, Java, Python), or they require the programmer to manually allocate and free memory (C, C++). Both approaches have downsides — garbage collectors add runtime overhead and unpredictable pauses, while manual management is error-prone and leads to bugs such as use-after-free, double-free, and memory leaks.
Rust takes a third path. The compiler tracks who owns each piece of data through
a set of rules checked at compile time. There is no garbage collector, no
runtime cost, and no manual malloc/free. If your code
violates the ownership rules, it simply does not compile. This design eliminates
entire classes of bugs before the program ever runs.
The ownership system is built on three core rules:
- Each value in Rust has exactly one owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value is dropped (memory freed).
Closely related to ownership are borrowing and slices.
Borrowing lets you use a value without taking ownership of it, via references
(&T for shared access and &mut T for exclusive
mutable access). Slices are lightweight references to a contiguous portion of a
collection. Together these mechanisms give you precise control over memory
without sacrificing safety or ergonomics.
In Rust, a move occurs when the compiler transfers ownership of a value from one
binding to another, typically during assignment or when passing arguments by
value. A move does not copy the underlying data; instead, it reassigns
responsibility for managing the value's lifetime to the new binding. After the
transfer, the previous binding is considered invalid, and any subsequent use is
rejected at compile time. This move semantics—applied to all types that do not
implement Copy—is fundamental to Rust's memory model, enabling
deterministic resource management and preventing use-after-free and double-free
errors without requiring a garbage collector.
Moves and borrows serve different purposes in Rust's ownership system. A move
transfers ownership of a value to a new binding, invalidating the previous one
and determining which scope is responsible for running Drop.
Borrowing, by contrast, creates references that grant temporary access without
transferring ownership. Immutable borrows allow shared read-only access, while
mutable borrows enforce exclusive access to guarantee aliasing safety. Together,
move semantics and the borrow checker ensure deterministic resource management,
prevent data races, and eliminate use-after-free errors at compile time.
| Concept | Ownership | Aliasing | Mutability | After use |
|---|---|---|---|---|
| Move | Transferred | No aliasing | Depends on new owner | Old binding invalid |
| Immutable borrow | Retained by owner | Many aliases allowed | Read-only | Ends when reference goes out of scope |
| Mutable borrow | Retained by owner | Exclusive alias | Read/write | Ends when reference goes out of scope |
The table highlights how ownership, aliasing, mutability, and the lifecycle of references differ between moves and borrows in Rust.
Ownership
Ownership is Rust's central feature for memory safety without a garbage collector. Every value has exactly one owner — a variable that is responsible for it. When the owner goes out of scope, Rust automatically drops the value and frees its memory.
fn main() {
let s = String::from("hello"); // s owns the String on the heap
print_string(s); // ownership moves into print_string
// println!("{}", s); // error: value moved, s is invalid
}
fn print_string(text: String) { // text is the new owner
println!("{}", text);
} // text goes out of scope → String is dropped
The variable s owns a heap-allocated String. Passing
s to print_string moves ownership into the
function — s is no longer valid after that call. Inside
print_string, text is the owner. When the function
returns, text goes out of scope and Rust drops the
String, freeing the heap memory automatically — no garbage collector
needed.
λ cargo run -q hello
Move semantics
When a value is assigned to another variable, ownership is moved. The original variable can no longer be used.
fn main() {
let s1 = String::from("hello");
let s2 = s1; // ownership moves to s2
// println!("{}", s1); // error: value borrowed after move
println!("{}", s2);
}
The String is moved from s1 to s2.
let s2 = s1;
After the move, s1 is invalid. Trying to use it would cause a
compile-time error. If you need a deep copy, use clone.
λ cargo run -q hello
Borrowing (immutable references)
Instead of moving a value, you can pass a reference to it. This is called borrowing. The original owner keeps the value.
fn main() {
let s = String::from("hello");
let len = calculate_length(&s); // borrow s
println!("The length of '{}' is {}.", s, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
The calculate_length function borrows the string. The
& symbol creates a reference that does not take ownership.
Multiple immutable references to the same data are allowed.
λ cargo run -q The length of 'hello' is 5.
Mutable borrowing
A &mut reference allows modification of the borrowed value. Rust
enforces that only one mutable reference exists at a time.
fn main() {
let mut s = String::from("hello");
append_world(&mut s);
println!("{}", s);
}
fn append_world(s: &mut String) {
s.push_str(", there");
}
The append_world function takes a mutable reference to the string
and modifies it in place. The borrow ends when the reference goes out of scope,
allowing the original owner to continue using the value safely.
fn append_world(s: &mut String) {
s.push_str(", there");
}
The function receives a mutable reference and can change the string in place. The borrow ends when the reference goes out of scope, so the original owner can continue using the value.
λ cargo run -q hello, there
Slices
A slice is a reference to a contiguous part of a collection. It borrows
a window into the data without taking ownership. The most common slice type is
&str — a string slice that points into an existing
String or a string literal.
fn main() {
let s = String::from("hello there");
let word = first_word(&s);
println!("First word: {}", word);
}
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[..i]; // slice up to the space
}
}
&s[..] // no space found — return the whole string
}
The function accepts a string slice &str and returns a
&str that borrows part of the same data — no allocation, no
copy. The original String in main remains owned by
s throughout.
s.as_bytes() gives a byte view of the string.
bytes.iter().enumerate() walks each byte together with its index
i. When a space (b' ') is found, &s[..i]
returns a slice from the start up to — but not including — that position. If the
string contains no space, &s[..] returns a slice of the whole
string.
λ cargo run -q First word: hello
Array Slices
A slice can reference a contiguous portion of an array. The slice type
&[T] borrows a window into the array without copying the
elements.
fn main() {
let nums = [1, 2, 3, 4, 5];
let middle = &nums[1..4]; // borrows elements at index 1, 2, 3
println!("{:?}", middle);
print_sum(middle);
}
fn print_sum(slice: &[i32]) {
let total: i32 = slice.iter().sum();
println!("Sum: {}", total);
}
&nums[1..4] creates a slice that borrows three elements from the
array. The original array nums retains ownership. The function
print_sum accepts &[i32] — it works with any
contiguous sequence of i32 values, whether the caller passes a full
array reference or a smaller slice.
λ cargo run -q [2, 3, 4] Sum: 9
Vector Slices
A Vec<T> can be sliced the same way as an array. Because
Vec<T> derefs to &[T], you can pass a vector
reference directly to any function that accepts a slice.
fn main() {
let scores = vec![88, 92, 75, 61, 95, 83];
let top = &scores[4..]; // borrows from index 4 to the end
let bottom = &scores[..2]; // borrows the first two elements
let middle = &scores[2..4]; // borrows elements at index 2 and 3
println!("Top: {:?}", top);
println!("Bottom: {:?}", bottom);
println!("Middle: {:?}", middle);
}
The three range forms — start.., ..end, and
start..end — all produce a &[i32] slice. The end
index is exclusive in every case, so &scores[..2] yields
elements at index 0 and 1. The vector itself is never moved or copied.
λ cargo run -q Top: [95, 83] Bottom: [88, 92] Middle: [75, 61]
Mutable Slices
A mutable slice &mut [T] borrows a portion of a collection with
write access. The same exclusivity rule that applies to mutable references
applies here — only one mutable slice into a collection may exist at a time.
fn main() {
let mut nums = [10, 20, 30, 40, 50];
double_each(&mut nums[1..4]); // mutably borrows elements 1, 2, 3
println!("{:?}", nums);
}
fn double_each(slice: &mut [i32]) {
for x in slice.iter_mut() {
*x *= 2;
}
}
&mut nums[1..4] borrows three elements for mutation. Inside
double_each, iter_mut yields a mutable reference to
each element, and *x *= 2 dereferences and doubles it in place. The
array is modified directly — no values are returned or copied out.
λ cargo run -q [10, 40, 60, 80, 50]
Byte Slices
Raw binary data is represented as &[u8] — a slice of unsigned
bytes. File contents, network buffers, and encoded strings are all commonly
handled as byte slices.
fn main() {
let data: Vec<u8> = vec![0x52, 0x75, 0x73, 0x74]; // ASCII "Rust"
print_bytes(&data);
print_bytes(&data[..2]); // borrows the first two bytes
}
fn print_bytes(bytes: &[u8]) {
for byte in bytes {
print!("{:02X} ", byte);
}
println!();
}
print_bytes accepts any &[u8], so the same function
handles the full buffer or a sub-slice without any copying. The format specifier
{:02X} prints each byte as a zero-padded two-digit hex value.
λ cargo run -q 52 75 73 74 52 75
Clone — explicit deep copy
When you need two independent copies of heap data, call clone.
Unlike a move, clone duplicates the underlying data so both
variables remain valid. It is intentionally verbose because deep copies can be
expensive.
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // deep copy — s1 is still valid
println!("s1 = {}, s2 = {}", s1, s2);
}
Because clone copies the heap allocation, both s1 and
s2 have their own independent data. Dropping one does not affect
the other.
λ cargo run -q s1 = hello, s2 = hello
Copy types
Simple scalar types that live entirely on the stack implement the
Copy trait. When you assign a Copy type to another
variable, the value is copied bit-for-bit rather than moved. Both variables
remain usable — no explicit clone is needed.
fn main() {
let x: i32 = 5;
let y = x; // x is copied, not moved
println!("x = {}, y = {}", x, y);
let flag = true;
let other = flag; // bool is Copy too
println!("flag = {}, other = {}", flag, other);
}
Types that implement Copy include all integer types, floating-point
types, bool, char, and tuples composed entirely of
Copy types. Types that manage heap memory, such as
String or Vec<T>, do not implement
Copy.
λ cargo run -q x = 5, y = 5 flag = true, other = true
Ownership in functions
Passing a value to a function follows the same rules as assignment. Ownership
is moved into the function unless the type is Copy. Returning a
value from a function transfers ownership back to the caller.
fn takes_ownership(s: String) {
println!("Got: {}", s);
} // s is dropped here
fn makes_copy(n: i32) {
println!("Got: {}", n);
} // n goes out of scope; nothing special happens
fn gives_ownership() -> String {
String::from("yours now")
}
fn main() {
let s1 = String::from("hello");
takes_ownership(s1);
// println!("{}", s1); // error: s1 was moved
let n = 42;
makes_copy(n);
println!("n is still {}", n); // fine — i32 is Copy
let s2 = gives_ownership();
println!("{}", s2);
}
When s1 is passed to takes_ownership its ownership
moves into the function. n is an integer, so it is copied and the
original remains valid. gives_ownership constructs a
String and returns it, transferring ownership to the caller.
λ cargo run -q Got: hello Got: 42 n is still 42 yours now
Borrow rules — multiple readers or one writer
Rust enforces two complementary rules at compile time:
- You may have any number of immutable references
(
&T) to a value at the same time. - You may have exactly one mutable reference
(
&mut T) to a value, and while it exists no other reference of any kind may coexist.
These rules prevent data races entirely. The following example shows both patterns side by side.
fn main() {
let mut s = String::from("Rust");
// Many immutable references are fine at the same time.
let r1 = &s;
let r2 = &s;
println!("r1={}, r2={}", r1, r2);
// r1 and r2 are no longer used after this point.
// Now we can create a mutable reference.
let r3 = &mut s;
r3.push_str(" ownership");
println!("{}", r3);
}
The code first creates two immutable references, r1 and
r2, which are used to read the string. After the
println! that uses them, the compiler sees that they are no longer
used. At that point, it allows a mutable reference r3 to be
created, which modifies the string in place. This is safe because there are no
active immutable references at thetime the mutable reference is created.
let r3 = &mut s;
The compiler uses non-lexical lifetimes (NLL) to see that
r1 and r2 are not used after the first
println!, so the mutable borrow that follows is valid. Had you
tried to use r1 after creating r3, the compiler would
reject the program.
λ cargo run -q r1=Rust, r2=Rust Rust ownership
Borrowing in loops
A common beginner pitfall is trying to mutate a collection while iterating over it. Consider this code:
// This code does NOT compile — shown for illustration only.
fn main() {
let mut s = String::from("abc");
for c in s.chars() {
s.push(c); // ERROR: cannot borrow `s` as mutable
// because it is also borrowed as immutable
}
println!("{}", s);
}
Why does this fail? The for loop calls s.chars(),
which returns an iterator that holds an immutable borrow of
s. This borrow lives for the entire duration of the loop.
Inside the loop body you then try to call s.push(c), which requires
a mutable borrow. The borrow checker rejects this because you
cannot have a mutable borrow while an immutable borrow is still active. The
compiler error message spells this out clearly:
λ cargo run -q
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
--> simple.rs:6:9
|
5 | for c in s.chars() {
| ---------
| |
| immutable borrow occurs here
| immutable borrow later used here
6 | s.push(c); // ERROR: cannot borrow `s` as mutable
| ^^^^^^^^^ mutable borrow occurs here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `simple` (bin "simple") due to 1 previous error
There are several ways to fix this, depending on what you need to achieve.
Collect into a new string
If you want to double the characters (for example), build the result in a
separate String and reassign:
fn main() {
let s = String::from("abc");
let mut result = String::new();
for c in s.chars() {
result.push(c);
result.push(c);
}
println!("{}", result);
}
This works because result is a new string and does not overlap with
the borrow of s.
λ cargo run -q aabbcc
Use retain for filtering
If you only need to remove some characters based on a condition,
retain does the iteration and mutation in one safe operation:
fn main() {
let mut s = String::from("a1b2c3");
s.retain(|c| c.is_alphabetic());
println!("{}", s);
}
The retain method takes a closure that returns a boolean. It keeps
the character if the closure returns true and removes it if the
closure returns false. It is implemented in a way that does not
require mutable access to the string while iterating, so it does not violate the
borrow rules.
λ cargo run -q abc
Use push_str or extend
If you want to append a copy of the whole string to itself, use a method that borrows immutably:
fn main() {
let mut s = String::from("abc");
let copy = s.clone();
s.push_str(©);
println!("{}", s);
}
λ cargo run -q abcabc
Use indices
When you need random access or want to avoid holding an iterator borrow, index
with a plain for loop over a range. This works because indexing
creates a fresh borrow on each iteration instead of holding one borrow across
the entire loop:
fn main() {
let mut v = vec![1, 2, 3, 4];
for i in 0..v.len() {
v[i] *= 2; // fine: no iterator borrow
}
println!("{:?}", v);
}
λ cargo run -q [2, 4, 6, 8]
Vec<T> (O(1)
random access), but indexing a String by byte position is fragile
because Rust strings are UTF-8 encoded and a single char may span
multiple bytes. Use .char_indices() or collect into a
Vec<char> if you need character-level indexing for strings.
Dangling references
In languages with manual memory management it is easy to return a pointer to memory that has already been freed — a dangling pointer. Rust prevents this at compile time. A reference must always be valid for its entire lifetime.
// This code does NOT compile — shown for illustration only.
//
// fn dangle() -> &String {
// let s = String::from("hello");
// &s // ERROR: s is dropped at end of function
// } // returning a reference to dropped memory
// Correct version: return the owned String instead.
fn no_dangle() -> String {
let s = String::from("hello");
s // ownership moves to the caller — no dangling reference
}
fn main() {
let s = no_dangle();
println!("{}", s);
}
The compiler tracks that the reference returned by the commented-out
dangle function would outlive the local variable s
and rejects it with a lifetime error. The fix is to return the
String by value, moving ownership to the caller so the data
remains valid.
λ cargo run -q hello
Author
List all Rust tutorials.