The whiteboard rule
You're building a text editor. The user types a character. The undo stack needs to record the change. The syntax highlighter needs to scan the buffer to color keywords. The cursor position needs to update based on the new length. Three different parts of your code touch the same text buffer at the exact same moment. In C++, you'd grab a mutex and hope you didn't deadlock. In Python, you'd just let the GIL handle it and move on. In Rust, the compiler stops you before you even think about a mutex. It forces you to decide: is this data being read, or is it being written? You can't do both at once.
Borrowing is Rust's way of tracking who is looking at your data and who is changing it. The rule is simple but strict: you can have any number of readers, or exactly one writer. Never both.
Think of a whiteboard in a meeting room. Anyone can walk in and read what's written. That's an immutable borrow. You can have ten people reading the whiteboard at once. No problem. But if someone wants to erase a line and write new text, they need exclusive access. They put up a "Do Not Disturb" sign. While that sign is up, no one else can look at the board. Once they finish writing and take the sign down, the board is readable again.
In Rust code, &T is a shared reference. It's a reader. &mut T is a mutable reference. It's a writer. The compiler enforces the whiteboard rules at compile time. No runtime checks. No locks. Just structure.
The whiteboard analogy holds up: readers don't interfere with readers, but writers need the room to themselves.
Minimal example
fn main() {
let mut data = String::from("Rust");
// &data creates a shared reference.
// Multiple shared references can coexist safely.
let reader1 = &data;
let reader2 = &data;
println!("Readers see: {}, {}", reader1, reader2);
// &mut data creates an exclusive reference.
// All previous readers must be done before this line.
let writer = &mut data;
writer.push_str(" is cool");
println!("Writer changed it to: {}", writer);
}
The compiler tracks the last use of a variable, not just where it's declared.
How the compiler tracks borrows
The compiler doesn't just look at scope blocks. It looks at where variables are last used. This feature is called non-lexical lifetimes. In the example above, reader1 and reader2 are used in the println! macro. After that line, the compiler knows no one is reading data anymore. It allows writer to take over.
If you tried to print reader1 after writer was created, the compiler would reject the code. The borrow checker tracks the flow of data, not just the braces. This makes borrowing much more ergonomic than it used to be. You don't need empty braces to drop references early. The compiler figures it out.
Structure your code so reads happen, then writes happen. The compiler will enforce the order.
Realistic example
/// Calculates the sum of a slice of integers.
/// Takes a shared reference to avoid moving the data.
fn calculate_sum(values: &[i32]) -> i32 {
let mut total = 0;
// Iterate over the slice without taking ownership.
for &val in values {
total += val;
}
total
}
/// Appends a new value to the vector.
/// Requires exclusive access to modify the collection.
fn append_value(vec: &mut Vec<i32>, val: i32) {
vec.push(val);
}
fn main() {
let mut numbers = vec![10, 20, 30];
// Borrow immutably to calculate the sum.
let sum = calculate_sum(&numbers);
// The immutable borrow ends here.
// We can now borrow mutably to append the sum.
append_value(&mut numbers, sum);
println!("Result: {:?}", numbers);
}
Convention aside: When you call a method that returns a reference, check the lifetime. If the method returns &T, it usually borrows from self. If it returns T, it takes ownership. Read the signature. The return type tells you who owns the result.
Methods and self
When you write structs, you'll see &self and &mut self in method signatures. These are shorthand for borrowing the struct. &self means the method reads the struct but doesn't change it. &mut self means the method modifies the struct.
Choose &self whenever possible. It allows the caller to hold a reference to the struct while calling the method. If you use &mut self, the caller must have exclusive access. This restriction ripples through your code. A method that takes &self is easier to use. It can be called from multiple threads if the struct is wrapped in Arc, and it doesn't block other reads. Reserve &mut self for methods that actually update state.
Write &self by default. Switch to &mut self only when the method changes state.
Pitfalls and compiler errors
The most common error is E0502: cannot borrow as mutable because it is also borrowed as immutable. This happens when you hold a reference and try to mutate the original value.
let mut s = String::from("hello");
let r1 = &s;
// Error: cannot borrow `s` as mutable because it is also borrowed as immutable
let r2 = &mut s;
The compiler sees r1 is still alive. It refuses r2. The fix is usually structural. Use r1 before creating r2. Or scope the read.
let mut s = String::from("hello");
{
let r1 = &s;
println!("{}", r1);
} // r1 drops here
let r2 = &mut s; // Now safe
Another pitfall is borrowing part of a struct while mutating the whole struct. Rust allows partial borrowing. You can borrow a field mutably while the rest of the struct is borrowed immutably, as long as the fields don't overlap. This is useful for large structs where you only need to update one piece of data.
When the borrow checker yells, look at the scope. The fix is usually moving a line of code, not fighting the language.
When to use what
Use &T when you only need to read the data and want to allow other parts of the code to read it too. Use &T for function arguments that inspect values without modifying them. Use &mut T when you need to change the data and no one else should be looking at it during the change. Use &mut T for functions that update state, append to collections, or reassign fields. Use ownership (T) when the function takes full responsibility for the value and won't return it, or when you need to move data across threads without synchronization. Reach for references whenever you can. Moving data is expensive and restrictive.
Prefer references. They're cheap, they're safe, and they keep your data where it belongs.