ZetCode

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:

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.

main.rs
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.

main.rs
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.

main.rs
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.

main.rs
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.

main.rs
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.

main.rs
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.

main.rs
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.

main.rs
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.

main.rs
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.

main.rs
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.

main.rs
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.

main.rs
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:

These rules prevent data races entirely. The following example shows both patterns side by side.

main.rs
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:

main.rs
// 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:

main.rs
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:

main.rs
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:

main.rs
fn main() {
    let mut s = String::from("abc");
    let copy = s.clone();
    s.push_str(&copy);
    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:

main.rs
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]
Note: Indexing works well for 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.

main.rs
// 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

This tutorial was written by an experienced Rust developer. It is part of a series that explains core Rust concepts step by step.

List all Rust tutorials.