Level 2 · The Rust Way

Borrowing & References 🤝

Last lesson, giving a value to a function meant losing it. Ridiculous, right? Meet the fix: lending. This is where Rust starts feeling good.

Lending instead of giving: &

Put an ampersand in front of a value and you create a reference: a way to look at a value without owning it. Think of a library book 📚: you can read it, but the library still owns it, and you have to give it back.

fn count_letters(text: &String) -> usize {
    text.len()
}   // the borrow ends here: nothing is freed, we never owned it

fn main() {
    let poem = String::from("crabs write free verse");
    let n = count_letters(&poem);   // lend it: &poem

    println!("{poem} has {n} letters");  // ✅ poem is still ours!
}

Compare with Lesson 6's devour: no ownership lost, no clunky hand-it-back dance. The & appears in two places: the function says "I accept borrowed Strings" (&String), and the caller says "here, borrow mine" (&poem). This is called borrowing.

Borrowing with edit rights: &mut

A plain & reference is read-only. To let someone modify your value without taking it, lend it mutably:

fn add_signature(letter: &mut String) {
    letter.push_str(" - sincerely, Rusty 🦀");
}

fn main() {
    let mut letter = String::from("Dear ocean,");
    add_signature(&mut letter);      // lend with edit rights
    println!("{letter}");            // changed, still ours
}

(The variable itself must be mut too; you can't lend edit rights you don't have.)

The two rules of borrowing 📜

At any moment, you may have:
  • 🟢 any number of read-only references (&T), or
  • 🔴 exactly one mutable reference (&mut T), never both.

In short: many readers or one writer.

Why so strict? Imagine ten people silently reading a shared document while one person rewrites it: chaos, confusion, readers seeing half-edited sentences. Every data race and a whole family of crashes come from exactly that: reading and writing the same data at once. Most languages let it happen and hope. Rust makes it unrepresentable:

fn main() {
    let mut score = String::from("10 points");

    let r1 = &score;
    let r2 = &score;        // ✅ two readers, fine
    println!("{r1} {r2}");

    let w = &mut score;     // ✅ readers are done, one writer OK
    w.push_str("!!!");
    println!("{w}");
}

But overlap a reader and a writer, and the compiler steps in:

let mut score = String::from("10 points");
let reader = &score;
let writer = &mut score;    // ❌ can't write while someone's reading
println!("{reader}");
error[E0502]: cannot borrow `score` as mutable because it is
              also borrowed as immutable
  |
2 |     let reader = &score;
  |                  ------ immutable borrow occurs here
3 |     let writer = &mut score;
  |                  ^^^^^^^^^^ mutable borrow occurs here
4 |     println!("{reader}");
  |               -------- immutable borrow later used here
💡 The usual fix: reorder A borrow ends after its last use. Finish reading before you start writing: move println!("{reader}") above the &mut line and the example compiles. When the borrow checker complains, ask: "can I finish one job before starting the next?" The answer is almost always yes.

Slices: borrowing a piece 🍰

You can also borrow just a portion of something. This finally explains &str from Lesson 3; it's a borrowed view into text:

fn main() {
    let song = String::from("under the sea");
    let word: &str = &song[6..9];    // borrow characters 6,7,8
    println!("{word}");              // "the"

    let numbers = [10, 20, 30, 40, 50];
    let middle = &numbers[1..4];     // borrow a window: [20, 30, 40]
    println!("{middle:?}");
}
📝 Why functions take &str, not &String A &str can view a whole String, a piece of one, or a "literal", so fn shout(text: &str) accepts all three. It's the friendlier doorway, and Rust converts &String to &str automatically. Idiom to remember: own a String, borrow a &str.
⚠️ Common stumbles
  • Forgetting the & at the call site: count_letters(poem) moves the value; count_letters(&poem) lends it.
  • Trying &mut on a variable that isn't mut.
  • Two &mut borrows alive at once; finish one job first.
Exercise 1

Fix it with a borrow

This is Lesson 6's broken cake program. Fix it by changing devour to borrow instead of take. You'll change exactly two characters.

fn devour(snack: String) {
    println!("nom nom: {snack}");
}

fn main() {
    let cake = String::from("carrot cake");
    devour(cake);
    println!("{cake} is still here!");
}
Reveal solution
fn devour(snack: &String) {          // 1: accept a loan
    println!("nom nom: {snack}");
}

fn main() {
    let cake = String::from("carrot cake");
    devour(&cake);                   // 2: lend, don't give
    println!("{cake} is still here!");
}

(Even more idiomatic: snack: &str; see the note above.)

Exercise 2

The decorator

Write fn decorate(s: &mut String) that wraps the text in sparkles (hint: push_str adds to the end; insert_str(0, "✨") adds to the front). Call it on a String of your choice and print the result.

Reveal solution
fn decorate(s: &mut String) {
    s.insert_str(0, "✨ ");
    s.push_str(" ✨");
}

fn main() {
    let mut title = String::from("The Rusty School");
    decorate(&mut title);
    println!("{title}");   // ✨ The Rusty School ✨
}
Exercise 3

Borrow-checker whisperer

This won't compile. Rearrange the lines (don't delete any!) so it does.

fn main() {
    let mut inventory = String::from("rope");
    let peek = &inventory;
    inventory.push_str(", torch");
    println!("peeked: {peek}");
    println!("final: {inventory}");
}
Reveal solution
fn main() {
    let mut inventory = String::from("rope");
    let peek = &inventory;
    println!("peeked: {peek}");        // finish reading FIRST
    inventory.push_str(", torch");     // then write
    println!("final: {inventory}");
}

The read-borrow peek ends at its last use, freeing inventory to be modified. Many readers or one writer, just not overlapping.