In the previous tutorial, we learned about ownership. We saw that passing a value to a function moves it, and you cannot use it anymore. That works, but it is limiting.

What if a function only needs to read the data? What if it needs to modify it but give it back? You should not have to move ownership every time.

This is where borrowing comes in. Borrowing lets you use a value without taking ownership of it. The value stays with the original owner.

What Is a Reference?

A reference is like a pointer — it points to data owned by someone else. But unlike raw pointers in C, Rust references are always valid. The compiler guarantees this.

You create a reference with &:

fn main() {
    let name = String::from("Alex");
    let length = calculate_length(&name);  // Borrow name
    println!("{} has {} characters", name, length);  // name still valid!
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

The &name creates a reference to name. The function calculate_length borrows the string — it can read it but does not own it. When the function ends, nothing is dropped because it never had ownership.

Stack                         Heap
┌──────────────┐
│ name: ptr ───┼────────────► [ A | l | e | x ]
│ len: 4       │         ▲
│ cap: 4       │         │
├──────────────┤         │
│ s: ptr ──────┼─────────┘  (s points to name, not to heap)
└──────────────┘

The reference s points to name, not directly to the heap data. When s goes out of scope, it does not free anything.

Immutable References: &T

By default, references are immutable. You can read data but not change it:

fn print_greeting(message: &String) {
    println!("Greeting: {}", message);
    // message.push_str("!");  // ERROR: cannot modify through &String
}

fn main() {
    let greeting = String::from("Hello");
    print_greeting(&greeting);
    println!("Still have: {}", greeting);
}

You can have many immutable references at the same time. This is safe because nobody is changing the data:

fn main() {
    let data = String::from("shared data");

    let r1 = &data;
    let r2 = &data;
    let r3 = &data;

    println!("{}, {}, {}", r1, r2, r3);  // All fine
}

Think of it like a library book that many people can read at the same time — as long as nobody is writing in it.

Mutable References: &mut T

To modify borrowed data, you need a mutable reference:

fn add_greeting(message: &mut String) {
    message.push_str(", welcome!");
}

fn main() {
    let mut name = String::from("Alex");
    add_greeting(&mut name);
    println!("{}", name);  // Alex, welcome!
}

Three things are required for mutable borrowing:

  1. The variable must be declared mut
  2. The reference must use &mut
  3. The function parameter must accept &mut

The Borrowing Rules

Rust enforces two rules at compile time:

  1. You can have EITHER one mutable reference OR any number of immutable references — but not both at the same time.
  2. References must always be valid — no dangling references.

Rule 1: One Mutable OR Many Immutable

This code fails:

fn main() {
    let mut data = String::from("hello");

    let r1 = &data;        // Immutable borrow
    let r2 = &mut data;    // ERROR: mutable borrow while immutable exists

    println!("{}, {}", r1, r2);
}
error[E0502]: cannot borrow `data` as mutable because it is
              also borrowed as immutable

And you cannot have two mutable references at the same time:

fn main() {
    let mut data = String::from("hello");

    let r1 = &mut data;
    let r2 = &mut data;  // ERROR: second mutable borrow

    println!("{}, {}", r1, r2);
}
error[E0499]: cannot borrow `data` as mutable more than once
              at a time

Why This Rule Exists

This rule prevents data races at compile time. A data race happens when:

  1. Two or more pointers access the same data at the same time
  2. At least one of them is writing
  3. There is no synchronization

In C or C++, data races cause bugs that are hard to find — they happen randomly and often only in production. Rust makes them impossible by checking at compile time.

Non-Lexical Lifetimes (NLL)

The Rust compiler is smart. A reference’s “lifetime” ends at the point where it is last used, not at the end of the block. So this is fine:

fn main() {
    let mut data = String::from("hello");

    let r1 = &data;
    let r2 = &data;
    println!("{} and {}", r1, r2);
    // r1 and r2 are no longer used after this point

    let r3 = &mut data;  // OK! Immutable refs are already done
    r3.push_str(" world");
    println!("{}", r3);
}

This works because r1 and r2 are not used after the first println!. The compiler sees this and allows the mutable borrow.

Dangling References

A dangling reference points to memory that has been freed. Rust prevents this at compile time:

fn dangle() -> &String {  // ERROR: returns a reference to dropped data
    let s = String::from("hello");
    &s  // s is dropped at end of function — reference would be invalid
}
error[E0106]: missing lifetime specifier

The fix is to return the owned value instead:

fn no_dangle() -> String {
    let s = String::from("hello");
    s  // Transfer ownership to caller
}

Borrowing with Slices

References work with slices too. A string slice &str is a reference to part of a String:

fn first_word(text: &str) -> &str {
    for (i, ch) in text.chars().enumerate() {
        if ch == ' ' {
            return &text[..i];
        }
    }
    text
}

fn main() {
    let sentence = String::from("hello world");
    let word = first_word(&sentence);
    println!("First word: {}", word);  // hello
}

Notice the parameter is &str not &String. This is more flexible — it accepts both &String and &str. This is a common Rust pattern.

UTF-8 note: This first_word function works correctly for ASCII text. For strings with multi-byte Unicode characters (like emoji or CJK), indexing by char position differs from byte position. Use .char_indices() instead of .enumerate() for full Unicode safety.

Mutable References in Practice

Here is a practical example — a function that filters a list in place:

fn remove_short_names(names: &mut Vec<String>, min_length: usize) {
    names.retain(|name| name.len() >= min_length);
}

fn main() {
    let mut team = vec![
        String::from("Alex"),
        String::from("Jo"),
        String::from("Sam"),
        String::from("Jordan"),
    ];

    remove_short_names(&mut team, 4);
    println!("Team: {:?}", team);  // ["Alex", "Jordan"]
}

The function borrows team mutably, modifies it, and returns. team is still owned by main.

References to Primitives

Copy types also work with references. Sometimes you want to avoid copies for large arrays or when you need the function to modify the original:

fn double_in_place(value: &mut i32) {
    *value *= 2;  // Dereference with * to modify the actual value
}

fn main() {
    let mut score = 50;
    double_in_place(&mut score);
    println!("Score: {}", score);  // 100
}

The * is the dereference operator. You need it to access the value behind the reference.

Lifetimes Preview

Sometimes the compiler needs help figuring out how long references live. This is what lifetimes are for:

fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
    if s1.len() >= s2.len() {
        s1
    } else {
        s2
    }
}

fn main() {
    let result;
    let s1 = String::from("long string");
    {
        let s2 = String::from("short");
        result = longer(&s1, &s2);
        println!("Longer: {}", result);
    }
}

The 'a annotation tells the compiler: “the returned reference lives as long as the shortest of the two input references.” We will cover lifetimes in detail in a future tutorial.

Common Mistakes and Fixes

Mistake: Modifying While Iterating

// ERROR: cannot borrow as mutable while iterating
let mut items = vec![1, 2, 3];
for item in &items {
    if *item == 2 {
        items.push(4);  // ERROR!
    }
}

Fix: Collect what you need first, then modify:

let mut items = vec![1, 2, 3];
let needs_four = items.iter().any(|&x| x == 2);
if needs_four {
    items.push(4);
}

Mistake: Returning a Reference to a Local

// ERROR: cannot return reference to local variable
fn create_name() -> &str {
    let name = String::from("Alex");
    &name  // name is dropped here!
}

Fix: Return the owned value:

fn create_name() -> String {
    String::from("Alex")
}

Summary

ConceptSyntaxRules
Immutable reference&TMany allowed at once
Mutable reference&mut TOnly one at a time
Borrow ruleOne &mut OR many &, never both
Dereference*refAccess value behind reference
Slice&str, &[T]Reference to part of data
No dangling refsCompiler prevents invalid references

Source Code

View source code on GitHub →

What’s Next?

We now understand ownership and borrowing — the core of Rust’s memory model. Next, we learn structs and methods — how to create your own types and attach behavior to them. This is where Rust starts to feel like a real application language.

Next: Rust Tutorial #6: Structs and Methods