Rust Basics #4 References and Borrowing: & and &mut, Solving Borrow-Rule Compile Errors

5 min read

The last part ended with an awkward pattern: losing ownership on every function call and taking it back through the return value. References are the answer. A reference leaves ownership where it is and lends only the right to access the value — borrowing, in Rust’s vocabulary. And the single rule attached to borrowing carries the other half of Rust’s safety story, alongside ownership.

References: reading without ownership #

src/main.rs
fn count_chars(text: &String) -> usize {
    text.chars().count()
} // text is only a reference, so nothing is freed here

let summary = String::from("monthly summary");
let n = count_chars(&summary); // pass a reference with &
println!("{summary}: {n} chars");  // summary is still valid

&summary creates a reference pointing at the value instead of handing the value over. The function can read the value but is not its owner, so when the function ends nothing is freed and summary remains usable. The “hand it back through the return value” code from last time disappears entirely. Taking parameters by reference is the default form for functions that only read.

Mutable references: borrowing to modify #

When modification is needed, borrow with &mut. The lending variable itself must also be mut.

src/main.rs
fn append_signature(body: &mut String) {
    body.push_str("\nBest regards,");
}

let mut mail = String::from("Hello.");
append_signature(&mut mail);

The important detail is that &mut appears at the call site. In many languages you cannot tell from a call whether the function mutates its argument; in Rust, a potentially mutating pass is visible in the caller’s code.

The borrow rule: many readers, one writer #

References come with one rule. At any given time, any number of shared references (&) are allowed, but only one mutable reference (&mut) — and never both kinds at once. It is the guarantee that a value does not change while someone is reading it.

Here is an accident this rule actually prevents: iterating over a collection while appending to it, which in many languages is a subtle bug or a runtime exception.

src/main.rs
let mut items = vec![1, 2, 3];
for item in &items {          // items is borrowed immutably
    if *item == 2 {
        items.push(99);       // compile error: tries to borrow mutably
    }
}
Compile error
error[E0502]: cannot borrow `items` as mutable because it is also
              borrowed as immutable
  |
2 |     for item in &items {
  |                 ------ immutable borrow occurs here
4 |             items.push(99);
  |             ^^^^^^^^^^^^^^ mutable borrow occurs here

When push runs out of internal capacity, it relocates the entire contents to new memory — and at that moment the iterating reference points at freed memory. In C++ that is undefined behavior; in Java it is a runtime ConcurrentModificationException; in Rust it is a compile error. Move to multiple threads and the same rule becomes the device that blocks data races (one side writing while another reads) at compile time.

When you meet cannot borrow as mutable, the standard move is not to fight it but to narrow the borrow. A reference is considered alive only up to its last use, so finishing the reads first and moving the mutation later resolves most cases. For the example above: collect the values to add during iteration, then push them all after the loop ends.

Dangling references: you cannot point at freed memory #

src/main.rs
fn make_greeting() -> &String {
    let s = String::from("hello there");
    &s // compile error: s is freed when the function ends
}

Return a reference to a local variable and the value is freed the moment the function ends, leaving only the reference — the classic C bug of returning a stack address. The Rust compiler rejects every case where “the borrowed value dies before the reference.” Here the answer is to return the String itself, transferring ownership, rather than a reference. The full picture of this check — how long a reference stays valid — is lifetimes, the subject of Part 9.

Slices: borrowing just a part #

As a variation on references, a slice borrows a contiguous part of a collection.

src/main.rs
let scores = vec![90, 85, 72, 60];
let top: &[i32] = &scores[..2];    // borrow only the first two

let title = String::from("Rust Basics");
let first: &str = &title[..4];     // "Rust"

The string slice &str matters especially. A string literal ("hello") has exactly this type, and if a function only reads a string, the convention is to take &str rather than &String — because it accepts both a String and a literal. Rewriting the earlier count_chars into its practical form:

src/main.rs
fn count_chars(text: &str) -> usize {
    text.chars().count()
}

One caution: string slice indexes are byte positions, not character counts. ASCII characters are one byte each, but accented letters, CJK text, and emoji are not — so an arbitrary cut like &title[..4] panics if it lands in the middle of a character. This property of strings gets its full treatment in Part 7.

Summary #

  • A reference & is a borrow that reads a value without moving ownership. It is the default form for read-only parameters; borrow with &mut when modification is needed.
  • The borrow rule is one line: many shared references, one mutable reference, never both. Mutation during iteration and data races are blocked at compile time by this rule alone.
  • Fix cannot borrow as mutable by narrowing the borrow: finish reads first, gather mutations afterwards.
  • Returning a reference to a value about to be freed is rejected at compile time. Return ownership itself in that case.
  • Functions that read strings take &str rather than &String by convention — it accepts both literals and Strings.
  • Next: structuring data with structs and enums, plus Option and match, Rust’s replacement for null.
X