Borrowing & References

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Rust 1.99.0 and the 2024 edition, checked on 2026-10-06. A reference lets code use a value without owning it; creating one is called borrowing. Borrowing is what makes ownership practical: functions can read or modify data that stays owned by the caller. The borrow checker enforces a small set of rules that rule out dangling pointers and data races at compile time. This page builds on Ownership & Moves and leads into Lifetimes.

Shared and mutable references

Rust has two kinds of references:

  • a shared reference &T, created with &value: read-only access, and any number may exist at once;

  • a mutable reference &mut T, created with &mut value: read-write access, and it must be the only way to reach the value while it exists. The value itself must be declared mut to be borrowed mutably.

fn word_count(text: &str) -> usize {          // borrows: the caller keeps ownership
    text.split_whitespace().count()
}

fn add_edition(title: &mut String, n: u8) {   // borrows mutably: may modify in place
    title.push_str(&format!(" ({n}nd ed.)"));
}

fn main() {
    let mut title = String::from("Dune Messiah");
    let n = word_count(&title);
    add_edition(&mut title, 2);
    println!("{title} -- {n} words");          // title is still owned and usable here
}

A reference is a non-null, aligned pointer (a plain address for sized types; a "fat" pointer with a length or vtable for slices and trait objects). Dereferencing uses *, but method calls and field access auto-dereference, so explicit * is mostly needed for assignment through a &mut to a Copy value (*count += 1) and for comparisons.

The borrowing rules: aliasing XOR mutation

At any given point in the program, for any value, you can have either:

  • any number of shared references (&T), or

  • exactly one mutable reference (&mut T),

but not both. And every reference must be valid: it can never outlive the value it points to.

The first rule is often summarised as aliasing XOR mutation: data may be shared or mutated, never both at the same time. It is the reason Rust can rule out iterator invalidation in single-threaded code and data races in multi-threaded code with the same mechanism.

stateDiagram-v2 direction LR Owned: Owned (owner can read, write, move) Shared: Shared-borrowed (n >= 1 readers, no writes, no moves) Mut: Mutably borrowed (exactly one writer, owner frozen) [*] --> Owned: let mut v = ... Owned --> Shared: &v Shared --> Shared: another &v Shared --> Owned: last shared borrow used for the last time Owned --> Mut: &mut v Mut --> Owned: mutable borrow used for the last time Owned --> [*]: owner moved or goes out of scope

Two mutable borrows at once are error E0499; a mutable borrow while a shared one is alive is error E0502:

fn main() {
    let mut titles = vec![String::from("Dune"), String::from("Emma")];
    let first = &titles[0];           // shared borrow of `titles`...
    titles.push(String::from("Kindred")); // error[E0502]: cannot borrow `titles` as mutable because it is also borrowed as immutable
    println!("{first}");              // ...still used here
}

This is not pedantry: push may reallocate the vector’s buffer, which would leave first pointing at freed memory. In C++ the same code compiles and is undefined behaviour (iterator invalidation).

Non-lexical lifetimes

A borrow lasts from where it is created to where it is last used, not to the end of the enclosing block. This is called non-lexical lifetimes (NLL, the default since the 2018 edition), and it accepts much more code than a purely scope-based checker would:

fn main() {
    let mut titles = vec![String::from("Dune"), String::from("Emma")];
    let first = &titles[0];
    println!("first: {first}");       // last use of `first`: the shared borrow ends here
    titles.push(String::from("Kindred")); // fine: no borrow is alive anymore
    let r1 = &mut titles;
    r1.sort();                        // r1's last use
    let r2 = &mut titles;             // fine: r1 is no longer alive
    r2.dedup();
    println!("{titles:?}");
}

A more precise borrow checker, Polonius, is under development on nightly; it accepts some programs that NLL rejects (notably "conditionally return a borrow, otherwise mutate"), but it is not part of stable Rust.

Dangling references are impossible

The second rule — references must not outlive their referent — is checked with lifetimes. A function cannot return a reference to one of its own locals, and a reference cannot be stored somewhere that outlives the borrowed value:

fn main() {
    let longest: &String;
    {
        let title = String::from("Middlemarch");
        longest = &title; // error[E0597]: `title` does not live long enough
    } // `title` dropped here while still borrowed
    println!("{longest}");
}

Returning a reference to a local from a function fails with E0106 ("missing lifetime specifier") or E0515 ("cannot return reference to local variable"); the fix is almost always to return the owned value instead. A borrow of a temporary that is dropped at the end of the statement gives E0716:

fn main() {
    let words: Vec<&str>;
    words = String::from("Dune Messiah").split(' ').collect(); // error[E0716]: temporary value dropped while borrowed
    println!("{words:?}");
}

Source: Error E0716

The fix is to bind the temporary to a variable (let title = String::from(...); let words = title.split(...)...) so that it lives as long as the borrow.

Assigning to borrowed values: E0506

While a value is borrowed, the owner cannot assign to it either — the borrow would observe a change it was promised would not happen. That is E0506:

fn main() {
    let mut copies = 3;
    let r = &copies;
    copies = 4; // error[E0506]: cannot assign to `copies` because it is borrowed
    println!("{r}");
}

Source: Error E0506

Reference patterns and binding modes

Patterns can bind references to parts of a value instead of moving them out. There are three ways to say so:

  • ref name binds a shared reference, and ref mut name a mutable one;

  • a & pattern matches against a reference and binds what it points to (it dereferences, the opposite of ref);

  • match ergonomics (since the 2018 edition): when you match a reference against a non-reference pattern, the compiler automatically dereferences and switches the default binding mode to ref (or ref mut), so bindings become references.

#[derive(Debug)]
struct Loan {
    member: String,
    days: u16,
}

fn main() {
    let loan = Loan { member: "alice".into(), days: 21 };

    let Loan { ref member, days } = loan;       // explicit `ref`: borrow member, copy days
    println!("{member} {days}");

    let loan_ref = &loan;
    let Loan { member, days } = loan_ref;       // match ergonomics: member: &String, days: &u16
    println!("{member} {days}");

    let pairs = [(1, "Dune"), (2, "Emma")];
    for &(n, title) in &pairs {                 // `&` pattern: destructure through the reference
        println!("{n}: {title}");
    }

    let mut counts = vec![3, 1];
    for c in &mut counts {                      // c: &mut i32
        *c += 1;
    }
    if let Some(first) = counts.first_mut() {   // binding is &mut i32
        *first *= 10;
    }
    println!("{counts:?} {loan:?}");            // [40, 2] -- loan was never moved
}

The 2024 edition’s match-ergonomics reservations

Match ergonomics had some confusing corner cases, for example writing mut x or &x inside a pattern that was already in ref binding mode. The 2024 edition reserves those combinations so they can be given a clearer meaning in a future edition. In practice, in a 2024 crate:

  • writing ref, ref mut or mut explicitly on a binding is an error when the default binding mode is not "move";

  • writing a & or &mut pattern where the default binding mode is not "move" is an error.

cargo fix --edition rewrites affected patterns into the fully explicit form (adding & to the outer pattern). Typical code — if let Some(x) = &opt, for (k, v) in &map — is unaffected.

Reborrowing and two-phase borrows

Passing a &mut T to a function that takes &mut T does not move the mutable reference (which would make it unusable afterwards). The compiler inserts a reborrow, &mut *r, a new, shorter-lived mutable borrow derived from r. While the reborrow is alive r is frozen; afterwards r is usable again:

fn add_one(n: &mut u32) {
    *n += 1;
}

fn main() {
    let mut copies = 1;
    let r = &mut copies;
    add_one(r);          // implicit reborrow: add_one(&mut *r)
    add_one(r);          // r is still usable
    *r += 10;
    println!("{copies}"); // 13
}

Two-phase borrows make a common pattern compile: in v.push(v.len()), the method call first takes &mut v for push, then evaluates the argument v.len(), which needs &v. The compiler treats the mutable borrow as "reserved" until the arguments have been evaluated, so the shared borrow is allowed:

fn main() {
    let mut lengths = vec![1, 2];
    lengths.push(lengths.len()); // two-phase borrow: the &mut is activated after the argument is evaluated
    println!("{lengths:?}");     // [1, 2, 2]
}

Partial and split borrows

The borrow checker tracks borrows per path: borrowing one field of a struct does not borrow the others. Inside one function, you can hold &mut s.a and &mut s.b at the same time:

struct Catalog {
    titles: Vec<String>,
    log: Vec<String>,
}

fn main() {
    let mut c = Catalog { titles: vec!["Dune".into()], log: Vec::new() };
    let titles = &mut c.titles;  // borrows only c.titles
    let log = &mut c.log;        // a disjoint field: allowed
    titles.push("Emma".into());
    log.push(format!("added Emma, now {}", titles.len()));
    println!("{:?}", c.log);
}

The precision stops at function boundaries: a method that takes &mut self borrows the whole struct, so self.titles.iter() combined with a call to self.record(...) (another &mut self method) fails with E0502 even though the method only touches log. Azam (Advanced Rust, chapter 3) calls these the borrow limits of method chains. The usual fixes:

  • borrow the fields directly in the method body instead of calling another method;

  • make the helper an associated function that takes only the fields it needs (Self::record(&mut self.log, ...));

  • split the struct into smaller structs, each with its own methods;

  • collect what you need first (let n = self.titles.len();), then call the method.

struct Catalog {
    titles: Vec<String>,
    log: Vec<String>,
}

impl Catalog {
    fn record(log: &mut Vec<String>, msg: String) { // takes only the field it needs
        log.push(msg);
    }

    fn audit(&mut self) {
        for t in &self.titles {
            // self.log_title(t) taking &mut self would conflict with the loop's borrow (E0502)
            Self::record(&mut self.log, format!("checked {t}"));
        }
    }
}

fn main() {
    let mut c = Catalog { titles: vec!["Dune".into(), "Emma".into()], log: Vec::new() };
    c.audit();
    println!("{:?}", c.log);
}

Splitting slices

The borrow checker cannot see that &mut v[0] and &mut v[1] are disjoint, because indexing is a method call. The standard library provides safe splitting methods implemented with a little unsafe code internally:

fn main() {
    let mut shelf = [10, 20, 30, 40];

    let (left, right) = shelf.split_at_mut(2);      // two non-overlapping mutable slices
    left[0] += right[0];
    println!("{shelf:?}");                          // [40, 20, 30, 40]

    if let Ok([a, b]) = shelf.get_disjoint_mut([0, 3]) { // 1.86: several &mut by index
        std::mem::swap(a, b);
    }
    println!("{shelf:?}");

    if let Some((first, rest)) = shelf.split_first_mut() {
        *first = rest.iter().sum();
    }
    println!("{shelf:?}");                          // [90, 20, 30, 40]
}

get_disjoint_mut (stabilised in 1.86, also available on HashMap) returns an error if any two indices are equal or out of bounds. Iterating with iter_mut() also yields disjoint mutable references, one element at a time.

How Rust compares with other languages

Swift reaches a similar goal at run time with exclusivity enforcement (see Swift: Memory Safety & Unsafe Pointers), while C and C++ pointers carry no aliasing rules at all apart from type-based aliasing; compare C: Pointers and C++: Move Semantics & Value Categories. Rust’s shared/mutable split is also what lets the compiler mark references noalias for LLVM, enabling optimisations C compilers only get with restrict.

Common errors

Error Meaning and usual fix

E0499

two mutable borrows alive at once: end the first borrow before creating the second, or split the borrow (fields, split_at_mut)

E0502

mutable borrow while shared borrows are alive (or the reverse): finish using the shared borrow first; copy the value you need out

E0506

assignment to a borrowed value: end the borrow first

E0597

the borrowed value does not live long enough: make the owner live longer, or store an owned value

E0716

a temporary is dropped while still borrowed: bind it to a variable first

E0596

mutable borrow of a non-mut binding: add mut

E0106

a reference in a signature needs a lifetime: see Lifetimes, or return an owned value

Where the books are outdated

Azam’s Advanced Rust (chapter 3) covers reference kinds, reference patterns and partial borrows; it predates the 2024 edition’s match-ergonomics reservations and get_disjoint_mut (1.86), which replaces many hand-written split_at_mut dances for non-adjacent indices.