The borrow checker
The rule is one line: aliasing XOR mutability. At any point in the program, a place has either any number of shared borrows, or exactly one exclusive borrow, never both.
var v: vec::Vec[i32] = vec::new::[i32]();
v.push(1);
let n: usize = v.len(); // shared borrow — fine
let p: i32 = vec::at_copy::[i32](v, 0 as usize); // shared borrow — fine
v.push(2); // exclusive — but no live shared borrow now; fine
Because shared borrows can read but not mutate, and the single exclusive borrow is the only one allowed to mutate, two threads or two code paths can never hold a writer and a reader of the same place at once. That is why data races do not compile.
The common diagnostics
- E0372: move out of a borrowed value (including moving a
Vecwhile a slice of it is live). - E0381: exclusive use of a place that is already borrowed (a field write, a
refargument, or a mutating method while a view or loan is live). - E0383: read while exclusively borrowed.
- E0370 family: overlapping incompatible borrows.
- E0513: a view of a dying owner escapes the function (returned directly, or inside a returned aggregate).
- E0514: a value dies while a scope or promoted raw pointer still holds a view of it.
- E0516: a view stored through a raw pointer (
*p = v, or(*p).f = v) is undeclared flow at the raw seam.
Views are tracked by shape
A str or T[] is a borrow written as a value. The checker follows the shape, not a method-name allowlist, through every form it can leak:
- Returning
t.view(),as_str/as_slice, or any user method that returnsstr/T[]from a borrowing receiver is E0513 when the owner is a local. - A
takeparameter ortake thisis a dying root: a returned view of it is E0513. return head(local)wherehead(x) -> strreturns a view of its parameter is rooted atlocal.let s = mk().view();(a view of a statement-scoped temporary) is rejected; passing such a view as a direct call argument stays legal.- Storing a view of a frame-dying owner into a
staticor arefout-parameter is rejected. - A view written into a field or index, produced by an
if/ block /matchexpression, or moved out by a struct destructure pins its owner (E0372 / E0381). - A slice of a
Vec[T](or any generic Drop type) pins it: moving it is E0372,appendwhile the slice is live is E0381. A read-only method alongside a live view stays legal. - A raw-pointer-returning function whose computed return flow carries a view-capable parameter has that flow promoted to a return borrow, so the caller ties the pointer to the argument's owner.
#[keeps(nothing)]opts a declared boundary out.
A mutating method call on a borrowed place was already refused; a plain field write, and passing the place as a ref argument, now are too.
The fix is almost always a scope boundary
When two borrows conflict, the fix is usually to end one before the other begins, by introducing a scope so they do not co-exist:
{
let r: i32 = vec::at_copy::[i32](v, 0 as usize);
#println(r);
} // shared borrow ends here
v.push(99); // exclusive borrow now fine
The borrow checker reasons within a function boundary. For the full picture of what it enforces versus what it trusts you to uphold (views and raw pointers), see Ownership.
Next
Continue with error handling.