What references are in Rust

A reference is a non-owning pointer to a value. Instead of moving the value into a function or copying it, you can pass a reference and let the function read or modify the original value under controlled rules.

Rust has two main kinds of references:

  • &T: immutable reference, used for read-only access
  • &mut T: mutable reference, used for exclusive write access

The key idea is simple:

  • Many immutable references can exist at the same time.
  • Only one mutable reference can exist at a time.
  • A mutable reference cannot coexist with any immutable references to the same value.

These rules are enforced at compile time, which helps prevent data races and invalid memory access.

Basic example

fn main() {
    let name = String::from("Rust");

    print_length(&name);

    println!("Original value is still available: {name}");
}

fn print_length(text: &String) {
    println!("Length: {}", text.len());
}

The function print_length borrows the string. It can inspect the value, but it does not take ownership, so name remains usable afterward.


Immutable borrowing with &

Immutable references are the default choice when a function only needs to read data. They are lightweight and allow multiple readers at once.

Why immutable borrowing matters

Suppose you want to compute something from a large string, vector, or struct. Taking ownership would force the caller to give up the value, even if the function only needs to inspect it. Borrowing avoids that cost and keeps the caller’s data available.

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

fn main() {
    let sentence = String::from("borrowed data is efficient");
    let word = first_word(&sentence);
    println!("{word}");
}

This example uses &str, not &String. That distinction is important: borrowing the string slice is more flexible because it accepts both String and string literals through deref coercion.

Prefer borrowed parameters when possible

A good rule is to accept the most general borrowed type that fits your needs:

  • Use &str for text input
  • Use &[T] for read-only access to a sequence
  • Use &T for shared access to a custom type

This makes your functions easier to reuse and reduces unnecessary allocations or cloning.


Mutable borrowing with &mut

Mutable references allow a function to change data in place. This is useful when you want to update a collection, normalize a value, or accumulate results without returning a new object every time.

Example: modifying a value in place

fn append_suffix(text: &mut String) {
    text.push_str("!");
}

fn main() {
    let mut message = String::from("Hello");
    append_suffix(&mut message);
    println!("{message}");
}

Notice two requirements:

  1. The variable must be declared mut.
  2. The reference passed to the function must be &mut message.

Both are necessary. The first allows the variable itself to be mutated. The second grants the function exclusive access.

Exclusive access is the point

Rust prevents mutable borrowing conflicts because simultaneous mutation is dangerous. If one part of the program can change a value while another part reads it, the result may be inconsistent or undefined in languages without strong safety checks.

Rust’s rule is stricter but safer: while a mutable borrow exists, no other borrow of the same value is allowed.


Borrowing rules in practice

The borrow checker enforces a few core rules that shape everyday Rust code.

RuleMeaningPractical impact
One mutable borrow at a timeExclusive write accessPrevents conflicting updates
Any number of immutable borrowsShared read accessEnables multiple readers
No mixing mutable and immutable borrowsReads and writes cannot overlapForces clear access boundaries

Example of valid borrowing

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

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

    println!("{r1} {r2}");
}

Multiple immutable references are fine because neither can modify the data.

Example of invalid borrowing

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

    let r1 = &data;
    let r2 = &mut data;

    println!("{r1}");
    println!("{r2}");
}

This does not compile because r1 and r2 overlap. Rust rejects the code before it can become a runtime bug.


Borrow scopes and non-lexical lifetimes

In modern Rust, a borrow ends when it is no longer used, not necessarily at the end of the block. This is called non-lexical lifetimes, and it makes code more ergonomic.

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

    let first = &value;
    println!("{first}");

    let second = &mut value;
    second.push_str(" world");

    println!("{second}");
}

Here, the immutable borrow ends after println!("{first}"), so the mutable borrow can begin afterward.

Practical takeaway

If you get a borrow error, check whether a reference is still being used later than you expect. Often the fix is to shorten the borrow’s scope by:

  • moving a println! earlier
  • introducing a smaller block
  • avoiding storing a reference longer than needed
fn main() {
    let mut value = String::from("hello");

    {
        let first = &value;
        println!("{first}");
    }

    let second = &mut value;
    second.push_str(" world");
}

The inner block makes the lifetime boundary explicit.


Borrowing slices and collections

Borrowing is not limited to strings. It is especially useful with vectors and arrays, where you often want to inspect or update part of a collection without taking ownership.

Read-only access to a slice

fn sum(values: &[i32]) -> i32 {
    values.iter().sum()
}

fn main() {
    let numbers = vec![1, 2, 3, 4];
    let total = sum(&numbers);
    println!("{total}");
}

The function accepts a slice, which is a borrowed view into a sequence. This is more flexible than requiring a Vec<i32>.

Mutable access to a slice

fn double_all(values: &mut [i32]) {
    for value in values {
        *value *= 2;
    }
}

fn main() {
    let mut numbers = vec![1, 2, 3, 4];
    double_all(&mut numbers);
    println!("{numbers:?}");
}

This pattern is common in data processing code because it updates values in place without allocating a new vector.


Common compiler errors and how to read them

Borrowing errors can look intimidating at first, but they usually point to a clear rule violation.

“cannot borrow as mutable because it is also borrowed as immutable”

This means you tried to create a mutable reference while an immutable one was still active.

Typical fixes:

  • stop using the immutable reference earlier
  • clone the data if you truly need independent ownership
  • restructure the code so reads happen before writes

“cannot move out of borrowed content”

This happens when you try to take ownership of a field or value through a reference. Borrowed data is not yours to move.

Instead, consider:

  • borrowing the field as &T
  • cloning the field if ownership is needed
  • using methods that operate on references

“borrowed value does not live long enough”

This often appears when a reference escapes the scope of the value it points to. The fix is usually to return owned data instead of a reference, or to ensure the original value lives long enough.


Designing APIs around borrowing

Good Rust APIs often use borrowing to make usage efficient and predictable. The goal is to accept references when ownership is unnecessary and reserve ownership for cases where the function must store or transform data independently.

Guidelines for function signatures

SituationRecommended parameter typeWhy
Read text&strWorks with String and string literals
Read a list&[T]Works with vectors and arrays
Modify text in place&mut StringAvoids allocation
Modify a list in place&mut [T]Efficient bulk updates
Store data beyond the callOwned TCaller transfers responsibility

Example: a practical parser helper

fn is_valid_username(name: &str) -> bool {
    let trimmed = name.trim();
    !trimmed.is_empty() && trimmed.len() >= 3
}

fn main() {
    let input = String::from("  alice  ");
    println!("{}", is_valid_username(&input));
}

The function only needs to inspect the input, so borrowing is the best choice.

Example: in-place normalization

fn normalize_username(name: &mut String) {
    let trimmed = name.trim().to_lowercase();
    *name = trimmed;
}

fn main() {
    let mut username = String::from("  Alice ");
    normalize_username(&mut username);
    println!("{username}");
}

Here mutation is appropriate because the function intentionally updates the caller’s value.


Best practices for using references

1. Borrow by default for read-only operations

If a function does not need ownership, take a reference. This keeps APIs efficient and reduces unnecessary cloning.

2. Use the most specific borrowed type that fits

Prefer &str over &String, and &[T] over &Vec<T>. This makes your functions more general and easier to compose.

3. Keep mutable borrows short

Short mutable borrow scopes make code easier to reason about and reduce borrow checker friction.

4. Avoid cloning just to satisfy the compiler

Cloning can be correct, but it should be intentional. If you are cloning only because the borrow checker complained, first consider whether the code structure can be improved.

5. Separate reading and writing phases

A common pattern is to collect all needed reads first, then perform mutations afterward. This aligns naturally with Rust’s borrowing rules.


When borrowing is not the right choice

Borrowing is powerful, but it is not always the best design.

Use ownership when:

  • the function must store the value for later use
  • the data outlives the caller’s scope
  • the function transforms data into a new owned result
  • the API needs to move values between threads or tasks

Borrowing is ideal when the function only needs temporary access. Ownership is better when the function becomes responsible for the data.


Summary

References are the everyday mechanism that lets Rust code access data safely without unnecessary copying. Immutable references support shared reads, mutable references support exclusive updates, and the borrow checker ensures those access patterns do not overlap in unsafe ways.

If you remember only a few rules, make them these:

  • Use &T for read-only access.
  • Use &mut T for in-place mutation.
  • Prefer borrowed inputs like &str and &[T] in APIs.
  • Keep borrow scopes short and explicit.
  • Let ownership handle cases where data must be stored or transferred.

Once these patterns become familiar, borrowing stops feeling restrictive and starts feeling like a design tool.

Learn more with useful resources