Skip to content
← All Rust articles

Rust explainer

Shared and mutable borrows

About 5 min to read. Allow extra time to work through examples.

Learn when borrows conflict, why separate fields can be borrowed together, and how to work with multiple mutable slice elements.

This article uses Rust. You can make it your practice mode to browse related content across the site.

A reference lets you use a value without taking ownership of it. The key question is whether that access is shared or exclusive:

  • &T provides shared access. Several shared borrows can overlap.
  • &mut T provides exclusive access. While that borrow is needed, you cannot independently access the same data through the owner or a conflicting reference.

This is often called aliasing XOR mutability: shared access or exclusive access to the same data. The phrase is useful, but needs two qualifications. You can temporarily reborrow through a mutable reference, and types such as Cell support controlled mutation through shared references. We will see both below.

Use the reference while the owner is borrowed

fn main() {
    let mut values = vec![1, 2];
    let borrowed = &mut values;
    borrowed.push(3);
    assert_eq!(borrowed.len(), 3);
    // No later use of borrowed, so we can use values again.
    values.push(4);
    assert_eq!(values, [1, 2, 3, 4]);
}

Calling borrowed.len() is allowed. A method taking &self can borrow through an existing mutable reference for the call. This is a reborrow; it does not give the owner a separate route to the data.

Check your understanding After let r = &mut v;, suppose r is used again later. Which operations can go between those two lines: v.len(), println!("{:?}", v), v.push(1), or r.len()? Show answer

Only r.len().

The first three operations need a new borrow of v that conflicts with the mutable borrow still needed by r. The last operation uses that existing access. Try reborrowing to explore this distinction.

Separate fields can be borrowed together

A place is a storage location, such as a variable, a field, or the value reached by dereferencing a pointer. Borrows conflict when their access overlaps; borrowing one field does not necessarily borrow every field.

struct Pair {
    left: Vec<i64>,
    right: Vec<i64>,
}

fn main() {
    let mut pair = Pair { left: vec![1], right: vec![2] };
    let left = &mut pair.left;
    let right = &pair.right;
    left.push(3);
    assert_eq!(right.len(), 1);
    assert_eq!(left, &[1, 3]);
}

The compiler can track these two struct fields separately. Indexing a vector is different. This example intentionally fails with E0499:

fn main() {
    let mut values = vec![1, 2, 3];
    let first = &mut values[0];
    let second = &mut values[1]; // Conflicts with the borrow needed below.
    *first += 10;
    *second += 20;
}

For Vec, mutable indexing uses IndexMut, borrowing the vector and returning a reference tied to that borrow. The borrow checker does not split these two indexed accesses into independent element borrows just because the indices differ.

The later uses matter. If neither reference were used after its creation, these lines would not demonstrate a conflict: the first borrow would no longer be needed when the second starts.

Use an API that provides disjoint access

split_at_mut divides a mutable slice into two non-overlapping parts:

fn main() {
    let mut values = [1, 2, 3];
    let (left, right) = values.split_at_mut(1);
    left[0] += 10;
    right[0] += 20;
    assert_eq!(values, [11, 22, 3]);
}

For individual elements, get_disjoint_mut checks the requested indices and returns an error if they overlap or are out of bounds:

fn main() {
    let mut values = [1, 2, 3];
    let [first, second] = values.get_disjoint_mut([0, 1]).unwrap();
    *first += 10;
    *second += 20;
    assert_eq!(values, [11, 22, 3]);
    assert!(values.get_disjoint_mut([0, 0]).is_err());
    assert!(values.get_disjoint_mut([0, 3]).is_err());
}

A mutable slice iterator also provides access to distinct elements. References returned by different calls to next() can remain usable together:

fn main() {
    let mut values = [1, 2];
    let mut iter = values.iter_mut();
    let first = iter.next().unwrap();
    let second = iter.next().unwrap();
    *first += 10;
    *second += 20;
    assert_eq!(values, [11, 22]);
}

Practice with two mutable borrows, mutable iteration, and splitting a slice.

Check your understanding Why can &mut pair.left and &pair.right coexist, while the vector indexing example fails? Show answer

The compiler tracks the two struct fields as separate places. Both vector indexing calls borrow the vector, and their returned references keep those borrows active through the later uses.

An API such as split_at_mut gives the caller disjoint references in one operation. Its implementation must uphold that guarantee; a function signature alone does not make an unsafe implementation correct.

A borrow can end before the block does

For these simple local references, the compiler can end the borrow after its last use:

fn main() {
    let mut values = vec![1];
    let first = &values[0];
    assert_eq!(*first, 1);
    values.push(2); // first is not used again.
    assert_eq!(values, [1, 2]);
}

Move the use of first after push, and the compiler rejects the program. push might reallocate the vector's storage, leaving the reference pointing into an old allocation. Reserving spare capacity does not make this conflicting borrow compile.

See non-lexical lifetimes and iterator invalidation for practice. Last use is a helpful starting point, but more complex cases also involve returned references, reborrows, and destructors.

The check happens before execution. Even this deliberately unreachable mutation is rejected with E0502:

fn main() {
    let mut values = vec![1];
    let first = &values[0];
    if false {
        values.push(2);
    }
    println!("{first}");
}

Do not assume that an optimization removing unreachable code also removes borrow-checking requirements. When a program is rejected, inspect which access conflicts and where the earlier borrow is still needed. Then shorten that borrow or choose an API that expresses the intended access.

Shared does not always mean unchanging

Interior-mutability types expose specific ways to change data through shared references. For example:

use std::cell::Cell;

fn main() {
    let count = Cell::new(1);
    let first = &count;
    let second = &count;
    first.set(2);
    assert_eq!(second.get(), 2);
}

Cell makes this safe through its API. RefCell instead checks its borrowing rules at runtime, and synchronization types such as Mutex coordinate access across threads. These types use UnsafeCell internally. That permits interior mutation through shared access; it does not permit two independently usable mutable references to the same value.

Reading the next borrow error

Identify three things: the data being borrowed, the access that conflicts, and the later operation keeping the first borrow active. Separate field access, a shorter borrow, or a slice API often resolves the problem without copying the data.

Borrowing works alongside ownership, lifetimes, and thread-safety traits to protect memory. It is not a complete explanation of every Rust safety rule. Unsafe code has additional obligations that the compiler does not prove for you.

Further reading: the Rust book on borrowing, slice APIs, and UnsafeCell's aliasing rules.

step 3 of 24

Ownership II: Borrowing and the Borrow Checker

Up next How to read a borrow-check diagnostic