References & Borrowing

Rust · Lesson 9 of 31 7 min read Updated

In the last lesson, passing a String to a function moved it, and getting it back meant returning it in a tuple. That is tiring. Most of the time a function only needs to use a value for a while, not own it. Rust lets you lend a value with a reference, and lending is called borrowing.

In this lesson you will learn shared and mutable references, the two borrowing rules the compiler enforces, and why those rules make whole classes of bugs impossible.

Shared references with &

A reference is like an address that points to a value owned by someone else. You create one with &. The owner keeps ownership, so nothing is moved and nothing is dropped when the reference goes away.

Rust
fn letter_count(word: &String) -> usize {
    word.len()
}

fn main() {
    let name = String::from("Kabir");
    let n = letter_count(&name);
    println!("{name} has {n} letters");
}
Output
Kabir has 5 letters

&name lends name to the function, and the parameter type &String says "a reference to a String". After the call, name is still valid in main. Compare this with count_letters in Ownership & Moves, which had to hand the String back.

References are immutable by default: through word the function can read the text but not change it. (In real code you would write the parameter as &str instead of &String, because it accepts more kinds of text; you will see why in Slices.)

Mutable references with &mut

To let a function change a borrowed value, lend it with &mut. The variable itself must be declared mut too.

Rust
fn add_topping(order: &mut String, extra: &str) {
    order.push_str(" + ");
    order.push_str(extra);
}

fn add_points(points: &mut u32, bonus: u32) {
    *points += bonus;
}

fn main() {
    let mut order = String::from("Paneer pizza");
    add_topping(&mut order, "extra cheese");
    add_topping(&mut order, "olives");
    println!("{order}");

    let mut points = 120;
    add_points(&mut points, 30);
    println!("Reward points: {points}");
}
Output
Paneer pizza + extra cheese + olives
Reward points: 150

In add_points, points is a reference, not a number, so you write *points to reach the value it points to. The * is the dereference operator. Method calls such as order.push_str(...) dereference automatically, which is why add_topping needs no *.

The borrowing rules

At any moment, for a given value, you can have either:

  • any number of shared references (&T), or
  • exactly one mutable reference (&mut T),

but never both at the same time. And every reference must always point to a valid value.

You want toAllowed at the same time
Read through several & referencesYes, as many as you like
Change through one &mut referenceOnly if no other reference is in use
Read through & while someone changes through &mutNever

Here is the rule being broken with two mutable borrows:

Rust
fn main() {
    let mut notes = String::from("Buy milk");
    let a = &mut notes;
    let b = &mut notes;
    a.push_str(", pay fees");
    b.push_str(", call Rohan");
}
Output
error[E0499]: cannot borrow `notes` as mutable more than once at a time
 --> src/main.rs:4:13
  |
3 |     let a = &mut notes;
  |             ---------- first mutable borrow occurs here
4 |     let b = &mut notes;
  |             ^^^^^^^^^^ second mutable borrow occurs here
5 |     a.push_str(", pay fees");
  |     - first borrow later used here

Why so strict? When two parts of a program can change the same data at once, or one part reads while another changes it, you get a data race: results that depend on timing and break in ways that are almost impossible to reproduce. In a single thread the same pattern causes bugs like the one in the Rust Introduction, where a vector moved its items while a reference still pointed at the old place. Rust rejects these programs before they run.

Borrows end at their last use

A borrow does not last until the end of the block. It ends after the last place the reference is used. The compiler feature that works this out is called non-lexical lifetimes (NLL). It makes many sensible programs compile:

Rust
fn main() {
    let mut marks = vec![67, 74, 81];

    let first = &marks[0];
    println!("First mark: {first}");   // last use of `first`

    marks.push(90);                    // fine: the shared borrow has ended
    println!("{marks:?}");
}
Output
First mark: 67
[67, 74, 81, 90]

If you moved the println! of first below marks.push(90), the shared borrow would still be alive during the push, and the program would fail with error E0502.

No dangling references

A dangling reference points at memory that has already been freed. Rust never lets one exist. Look at this function, which tries to return a reference to a local String:

Rust
fn make_greeting() -> &String {
    let text = String::from("Namaste");
    &text
}   // `text` is dropped here, so the reference would point at freed memory

The compiler stops with error[E0106]: missing lifetime specifier and explains that the return type contains a borrowed value, "but there is no value for it to be borrowed from". The fix is to return the owned value, so ownership moves out to the caller:

Rust
fn make_greeting() -> String {
    String::from("Namaste")
}

You will learn exactly how the compiler tracks how long references live in Lifetimes.

Common mistakes

  • Forgetting mut on the variable. &mut order needs let mut order.
  • Forgetting * on simple values. points += 30 does not work when points is &mut u32; write *points += 30.
  • Holding a reference too long. Keep borrows short, and finish reading through a & before you change the value.
  • Returning a reference to a local variable. Return the owned value (String, Vec) instead.

Key points

  • &value creates a shared (read-only) reference; &mut value creates a mutable one. Borrowing never moves ownership.
  • You may have many shared references or one mutable reference to a value at a time, never both.
  • These rules prevent data races and invalid references, and they are checked at compile time.
  • A borrow ends after its last use (non-lexical lifetimes), not at the end of the block.
  • Use * to reach the value behind a reference; method calls dereference automatically.

Exercise

Write a function apply_discount(price: &mut u32, percent: u32) that lowers a price in rupees by the given percentage (use integer maths). In main, create a mutable price of 1,500 rupees, apply a 10% discount and then a further 5% discount, and print the price after each step. Then write fn describe(price: &u32) that only prints the price, and call it at the end.

Show solution
Rust
fn apply_discount(price: &mut u32, percent: u32) {
    *price -= *price * percent / 100;
}

fn describe(price: &u32) {
    println!("Final price: Rs. {price}");
}

fn main() {
    let mut price = 1500;
    apply_discount(&mut price, 10);
    println!("After 10%: Rs. {price}");
    apply_discount(&mut price, 5);
    println!("After 5% more: Rs. {price}");
    describe(&price);
}
Output
After 10%: Rs. 1350
After 5% more: Rs. 1283
Final price: Rs. 1283

The first discount is 150, leaving 1350. Five percent of 1350 is 67.5, which integer division makes 67, leaving 1283. Each mutable borrow ends when its function call returns, so the shared borrow for describe is allowed.

Quick Quiz

1. What does & do in letter_count(&name)?

2. Which combination of references to one value is allowed at the same moment?

3. When does a borrow end in modern Rust?

4. How do you add 5 to the number behind n: &mut i32?

Learn Rust with a mentor

Free lessons get you started. Our Programming & Software Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call