Understand borrows within a function

The analysis that the compiler does to determine lifetimes and borrow check within a function body is quite complicated. A full exploration is beyond the scope of this guide, but we'll give a brief introduction here.

Your best bet if you run into an error you can't understand is to ask for help on the forum or elsewhere.

Borrow errors within a function

Here are some simple causes of borrow check errors within a function.

Recalling the Basics

The most basic mechanism to keep in mind is that &mut references are exclusive, while & references are shared and implement Copy. You can't intermix using a shared reference and an exclusive reference to the same value, or two exclusive references to the same value.

fn main() {
let mut local = "Hello".to_string();

// Creating and using a shared reference
let x = &local;
println!("{x}");

// Creating and using an exclusive reference
let y = &mut local;
y.push_str(", world!");

// Trying to use the shared reference again
println!("{x}");
}

This doesn't compile because as soon as you created the exclusive reference, any other existing references must cease to be valid.

Borrows are often implicit

Here's the example again, only slightly rewritten.

fn main() {
let mut local = "Hello".to_string();

// Creating and using a shared reference
let x = &local;
println!("{x}");

// Implicitly creating and using an exclusive reference
local.push_str(", world!");

// Trying to use the shared reference again
println!("{x}");
}

Here, push_str takes &mut self, so an implicit &mut local exists as part of the method call, and thus the example can still not compile.

Creating a &mut is not the only exclusive use

The borrow checker looks at every use of a value to see if it's compatible with the lifetimes of borrows to that value, not just uses that involve references or just uses that involve lifetimes.

For example, moving a value invalidates any references to the value, as otherwise those references would dangle.

fn main() {
let local = "Hello".to_string();

// Creating and using a shared reference
let x = &local;
println!("{x}");

// Moving the value
let _local = local;

// Trying to use the shared reference again
println!("{x}");
}

Referenced values must remain in scope

The effects of a value going out of scope are similar to moving the value: all references are invalidated.

fn main() {
let x;
{
    let local = "Hello".to_string();
    x = &local;
} // `local` goes out of scope here

// Trying to use the shared reference after `local` goes out of scope
println!("{x}");
}

Using &mut self or &self counts as a use of all fields

In the example below, left becomes invalid when we create &self to call bar. Because you can get a &self.left out of a &self, this is similar to trying to intermix &mut self.left and &self.left.

#![allow(unused)]
fn main() {
#[derive(Debug)]
struct Pair {
    left: String,
    right: String,
}

impl Pair {
    fn foo(&mut self) {
        let left = &mut self.left;
        left.push_str("hi");
        self.bar();
        println!("{left}");
    }
    fn bar(&self) {
        println!("{self:?}");
    }
}
}

More generally, creating a &mut x or &x counts as a use of everything reachable from x.

Some things that compile successfully

Once you've started to get the hang of borrow errors, you might start to wonder why certain programs are allowed to compile. Here we introduce some of the ways that Rust allows non-trivial borrowing while still being sound.

Independently borrowing fields

Rust tracks borrows of struct fields individually, so the borrows of left and right below do not conflict.

#![allow(unused)]
fn main() {
#[derive(Debug)]
struct Pair {
    left: String,
    right: String,
}

impl Pair {
    fn foo(&mut self) {
        let left = &mut self.left;
        let right = &mut self.right;
        left.push_str("hi");
        right.push_str("there");
        println!("{left} {right}");
    }
}
}

This capability is also called splitting borrows.

Note that data you access through indexing are not consider fields per se; instead indexing is an operation that generally borrows all of &self or &mut self.

fn main() {
let mut v = vec![0, 1, 2];

// These two do not overlap, but...
let left = &mut v[..1];
let right = &mut v[1..];

// ...the borrow checker cannot recognize that
println!("{left:?} {right:?}");
}

Usually in this case, one uses methods like split_at_mut in order to split the borrows instead.

Similarly to indexing, when you access something through "deref coercion", you're exercising the Deref trait (or DerefMut), which borrow all of self.

There are also some niche cases where the borrow checker is smarter, however.

fn main() {
// Pattern matching does understand non-overlapping slices (slices are special)
let mut v = vec![String::new(), String::new()];
let slice = &mut v[..];
if let [_left, right] = slice {
    if let [left, ..] = slice {
        left.push_str("left");
    }
    // Still usable!
    right.push_str("right");
}
}
fn main() {
// You can split borrows through a `Box` dereference (`Box` is special)
let mut bx = Box::new((0, 1));
let left = &mut bx.0;
let right = &mut bx.1;
*left += 1;
*right += 1;
}

The examples are non-exhaustive 🙂.

Reborrowing

As mentioned before, reborrows are what make &mut reasonable to use. In fact, they have other special properties you can't emulate with a custom struct and trait implementations. Consider this example:

#![allow(unused)]
fn main() {
fn foo(s: &mut String) -> &str {
    &**s
}
}

Actually, that's too fast. Let's change this a little bit and go step by step.

#![allow(unused)]
fn main() {
fn foo(s: &mut String) -> &str {
    let ms: &mut str = &mut **s;
    let rs: &str = &*s;
    rs
}
}

Here, both s and ms are going out of scope at the end of foo, but this doesn't invalidate rs. That is, reborrowing through references can impose lifetime constraints on the reborrow, but the reborrow is not dependent on references staying in scope! It is only dependent on the borrowed data.

This demonstrates that reborrowing is more powerful than nesting references.

Shared reborrowing

When it comes to detecting conflicts, the borrow checker distinguishes between shared reborrows and exclusive ones. In particular, creating a shared reborrow will invalidate any exclusive reborrows of the same value (as they are no longer exclusive). But it will not invalidated shared reborrows:

#![allow(unused)]
fn main() {
struct Pair {
    left: String,
    right: String,
}

impl Pair {
    fn foo(&mut self) {
        // a split borrow: exclusive reborrow, shared reborrow
        let left = &mut self.left;
        let right = &self.right;
        left.push('x');

        // Shared reborrow of all of `self`, which "covers" all fields
        let this = &*self;

        // It invalidates any exclusive reborrows, so this will fail...
        // println!("{left}");

        // But it does not invalidate shared reborrows!
        println!("{right}");
    }
}
}

Two-phase borrows

The following code compiles:

fn main() {
let mut v = vec![0];
let shared = &v;
v.push(shared.len());
}

However, if you're aware of the order of evaluation here, it probably seems like it shouldn't. The implicit &mut v should have invalidated shared before shared.len() was evaluated. What gives?

This is the result of a feature called two-phase borrows, which is intended to make nested method calls more ergonomic:

fn main() {
let mut v = vec![0];
v.push(v.len());
}

In the olden days, you would have had to write it like so:

fn main() {
let mut v = vec![0];
let len = v.len();
v.push(len);
}

The implementation slipped, which is why the first example compiles too. How far it slipped is hard to say, as not only is there no specification, the feature doesn't even seem to be documented 🤷.