Borrow-checking surprises

Published 2026-04-07

In the process of writing a borrow-checker I've often turned to see what rust does, and I've often been surprised and confused. My mental model of the borrow-checker turns out to be missing a lot of detail.

I've also shown these confusing examples to other people and they've given me all sorts of confidently wrong explanations, so I suspect most people who write rust regularly are missing these details too.

Once you see the answer it's easy to believe that you already knew, so try to explain each example before clicking reveal!

one

fn main() {
    let mut x = 0;
    let y = &mut x;
    *y = *y + 1;
}

The left-hand place expression evaluates to a mutable reference to x, but then the right-hand value expression reads from x. This shouldn't be allowed! Why does it work?

The right-hand expression is evaluated first, so the right-hand value expression reads from x before the left-hand place expression produces a mutable reference to x.

fn main() {    
    let mut x = 0;
    let y = &mut x;
    *({println!("lhs"); y}) = {println!("rhs"); *y + 1};
    // prints:
    //   rhs
    //   lhs
}

two

fn main() {    
    let mut x = 0;
    x += x;
}

So simple it's almost suspicious...

Ok, that one was obviously fine.

However, you may know that += is just sugar for add_assign. If we desugar the above example, does it look a little less obvious?

use std::ops::AddAssign;

fn main() {    
    let mut x = 0;
    x.add_assign(x);
}

Nah, it's still fine. But the . method call syntax is also sugar. Let's desugar that too:

use std::ops::AddAssign;

fn main() {    
    let mut x = 0;
    add_assign(&mut x, x);
}

error[E0503]: cannot use `x` because it was mutably borrowed
 --> src/main.rs:5:35
  |
5 |     AddAssign::add_assign(&mut x, x);
  |     --------------------- ------  ^ use of borrowed `x`
  |     |                     |
  |     |                     `x` is borrowed here
  |     borrow later used by call

Uh oh. Why is this different?

Rust has a feature called two-phase borrows that only activates in a few specific circumstances, one of which is the . method call syntax. The first x is initially taken as an immutable reference, then the other arugments are evaluated, then the immutable reference is 'activated' into a mutable reference.

This feature doesn't activate in the desugared version, so now x is immediately taken as a mutable reference in first argument and is not available to read from in the second argument.

three

fn main() {    
    let mut x = 0;
    let y = &mut x;
    z = y;
    *y = 1;
}

This one should be easy enough if you've written any rust.

error[E0382]: use of moved value: `y`
 --> src/main.rs:7:5
  |
5 |     let y = &mut x;
  |         - move occurs because `y` has type `&mut i32`, which does not implement the `Copy` trait
6 |     let z = y;
  |             - value moved here
7 |     *y = 1;
  |     ^^^^^^ value used here after move

This makes sense. We can't use y after we move it into z.

But what if we moved it into a function?

fn id(y: &mut usize) -> &mut usize {
    y
}

fn main() {    
    let mut x = 0;
    let y = &mut x;
    let z = id(y);
    *y = 1;
}

This is a trick question, right?

This one works! What's different?

id(y) is actually desugared into an implicit reborrow id(&mut *y). So y itself isn't moved, and z contains a new reference reborrowed from y. Then z is never used, so this reference is immediately killed and y becomes available again.

four

fn foo(a: &mut usize) -> &mut usize {
    let b = &mut *a;
    let c = &mut *b;
    return c;
}

We return c, but c is derived from b which won't outlive this function. Is that ok?

Perhaps surprisingly, this is fine. The lifetime of c is the same as a, so it's safe to return from the function, even it is derived from b and b is dropped at the end of the scope.

Which means it's fine to drop b ourselves?

fn foo(a: &mut usize)-> &mut usize {
    let b = &mut *a;
    let c = &mut *b;
    drop(b);
    return c;
}

error[E0505]: cannot move out of `b` because it is borrowed
 --> src/main.rs:7:10
  |
4 | fn foo(a: &mut usize)-> &mut usize {
  |           - let's call the lifetime of this reference `'1`
5 |     let b = &mut *a;
  |         - binding `b` declared here
6 |     let c = &mut *b;
  |             ------- borrow of `*b` occurs here
7 |     drop(b);
  |          ^ move out of `b` occurs here
8 |     return c;
  |            - returning this value requires that `*b` is borrowed for `'1`

Sadly explicit drops require being able to move the argument.


I'm sure I haven't found all the odd corners yet.

Perhaps this is part of what makes rust hard to learn? While the core borrow-checking rules are simple, there are so many exceptions (admittedly for solid ergonomic reasons) that it's easy to internalize the wrong rules.