RustBorrowing & References

Borrowing & References in Rust

Rust's ownership system is powerful, but if ownership were the only mechanism, working with data would be cumbersome — you'd have to pass values into functions and return them back just to continue using them. Borrowing solves this by letting you temporarily use a value without taking ownership of it.

The Problem With Moving

When you pass a value to a function in Rust, ownership moves into that function. Once the function returns, you no longer have access to that value — unless the function explicitly returns it back to you.

RUST
fn main() {
    let s = String::from("hello");

    let len = calculate_length(s); // ownership moves here

    // s is no longer valid — we can't use it!
    // println!("{}", s); // ERROR: value borrowed here after move
    println!("Length: {}", len);
}

fn calculate_length(s: String) -> usize {
    s.len()
} // s is dropped here

This works, but it's awkward. What if you need to use s again after calling the function? You'd have to return it alongside the length:

RUST
fn main() {
    let s = String::from("hello");

    // Transfer ownership in, get it back out
    let (s, len) = calculate_length(s);

    println!("'{}' has length {}", s, len); // now we can use s again
}

fn calculate_length(s: String) -> (String, usize) {
    let len = s.len();
    (s, len) // return ownership back to caller
}
Warning
This pattern works but creates verbose, awkward code. Having to return every value you borrow makes function signatures messy. Rust provides a much cleaner solution: borrowing with references.
What Is Borrowing?

Borrowing means creating a reference to a value. A reference lets you use a value without taking ownership of it. When the reference goes out of scope, the original value is not dropped — because the reference never owned it.

Think of it like lending a book to a friend. Your friend can read the book, but they don't own it. When they're done, the book comes back to you automatically.

You create a reference using the & operator:

RUST
fn main() {
    let s = String::from("hello");

    let len = calculate_length(&s); // pass a reference — not ownership

    println!("'{}' has length {}", s, len); // s still valid!
}

fn calculate_length(s: &String) -> usize { // s is a reference to a String
    s.len()
} // s goes out of scope, but nothing is dropped — it doesn't own the data
Success
By passing `&s` instead of `s`, we lend the value to the function. The function can use it, and when it's done, our original `s` is still perfectly valid.
Immutable References: &T

A plain reference &T is an immutable reference. It gives read-only access to the data. You cannot modify the value through an immutable reference — only read from it.

This is the default behaviour. References are immutable unless you explicitly opt in to mutability (covered in the next section).

RUST
fn main() {
    let s = String::from("hello");
    let r = &s; // r is an immutable reference to s

    println!("{}", r);       // fine — reading is allowed
    println!("{}", s);       // also fine — s is still the owner

    // r.push_str("!"); // ERROR: cannot borrow as mutable
    // *r = String::from("world"); // ERROR: cannot assign through &String
}
Note
An immutable reference is sometimes called a "shared reference" because multiple parts of your code can hold immutable references to the same value simultaneously — they're all just reading.
Creating and Using References

There are two sides to every reference:

  • Creating a reference: use & before the value (the caller's side)
  • Accepting a reference: use &T in the function parameter type (the callee's side)

The act of creating a reference is called borrowing.

RUST
fn greet(name: &String) {       // accepts a reference to String
    println!("Hello, {}!", name);
}

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

    greet(&name);   // borrow name — pass a reference
    greet(&name);   // borrow again — still valid!
    greet(&name);   // and again — immutable refs can be reused freely

    println!("name is still: {}", name); // ownership never left main
}
Multiple Immutable References

You can have any number of immutable references to a value at the same time. Since none of them can modify the data, they can safely coexist — there's no risk of one reference changing data while another is reading it.

RUST
fn main() {
    let s = String::from("hello");

    let r1 = &s; // first reference
    let r2 = &s; // second reference
    let r3 = &s; // third reference — all fine!

    println!("{}, {}, {}", r1, r2, r3); // all valid simultaneously
}
Tip
Multiple readers don't conflict with each other. This mirrors real-world intuition: many people can read the same document simultaneously without any problems.
References Don't Own the Data

A critical property of references: they do not own the data they point to. This means:

  • When a reference goes out of scope, the underlying data is not dropped
  • The original owner remains responsible for cleanup
  • The reference is only valid as long as the original owner is alive

RUST
fn main() {
    let s = String::from("hello"); // s owns the String

    {
        let r = &s;              // r borrows s
        println!("{}", r);       // use r
    }                            // r goes out of scope — String is NOT dropped
                                 // (r didn't own it)

    println!("{}", s);           // s still valid — it's still the owner
}                                // s goes out of scope — String IS dropped here
Dereferencing: *r

To access the value that a reference points to, you use the dereference operator *. In many situations Rust performs automatic dereferencing, so you don't need to write it explicitly — but understanding how it works is important.

RUST
fn main() {
    let x = 5;
    let r = &x;             // r is a reference to x

    println!("{}", r);      // Rust auto-derefs — prints 5
    println!("{}", *r);     // explicit deref — also prints 5

    // Comparison works with auto-deref
    if *r == 5 {
        println!("r points to 5");
    }

    // Auto-deref in method calls
    let s = String::from("hello");
    let sr = &s;
    println!("{}", sr.len()); // same as (*sr).len() — auto-deref
}
Note
Rust's auto-dereferencing means you rarely need to write `*` explicitly when calling methods or making comparisons. The compiler figures it out through a process called "deref coercion".
Dangling References: How Rust Protects You

In languages like C and C++, it's easy to create a dangling pointer — a reference to memory that has already been freed. This causes undefined behaviour and is a major source of security vulnerabilities and crashes.

Rust prevents dangling references entirely at compile time. The borrow checker ensures that a reference can never outlive the data it refers to.

RUST
// This does NOT compile in Rust
fn dangle() -> &String {         // trying to return a reference
    let s = String::from("hello"); // s is created here
    &s                           // try to return a reference to s
}                                // s is dropped here — reference would dangle!
error[E0106]: missing lifetime specifier
 --> src/main.rs:1:16
  |
1 | fn dangle() -> &String {
  |                ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value,
    but there is no value for it to be borrowed from

The compiler refuses to compile this code. The fix is to return the String itself, transferring ownership to the caller instead of trying to return a reference:

RUST
fn no_dangle() -> String {
    let s = String::from("hello");
    s            // move ownership out — caller takes responsibility for cleanup
}

fn main() {
    let s = no_dangle();
    println!("{}", s); // perfectly safe
}
Success
Rust's guarantee: if you have a reference, it will always point to valid, live data. Dangling references are impossible in safe Rust — the compiler rejects them entirely.
Slices as References

Slices are a special kind of reference. They refer to a contiguous sequence of elements in a collection rather than the whole collection. Two common slice types are:

  • &str — a string slice (a reference into a String or a string literal)
  • &[T] — a slice of a vector or array

RUST
fn main() {
    let s = String::from("hello world");

    let hello = &s[0..5];    // slice: bytes 0–4 → "hello"
    let world = &s[6..11];   // slice: bytes 6–10 → "world"

    println!("{} {}", hello, world); // hello world

    // String literals are already slices (&str)
    let literal: &str = "I am a string slice";

    // Vector slices
    let v = vec![1, 2, 3, 4, 5];
    let first_three: &[i32] = &v[0..3];
    println!("{:?}", first_three); // [1, 2, 3]
}
Tip
Prefer `&str` over `&String` in function parameters. A function accepting `&str` can handle both string slices and references to `String` values (via deref coercion), making it more flexible.

RUST
// Flexible: accepts both &str and &String (via deref coercion)
fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    for (i, &byte) in bytes.iter().enumerate() {
        if byte == b' ' {
            return &s[0..i]; // return slice up to first space
        }
    }
    &s[..] // no space found — return whole string
}

fn main() {
    let owned = String::from("hello world");
    let literal = "hello world";

    println!("{}", first_word(&owned));   // &String — works via deref coercion
    println!("{}", first_word(literal));  // &str — works directly
    println!("{}", first_word(&owned[6..])); // slice of String — also works
}
References and Lifetimes

Every reference has a lifetime — the scope for which it is valid. Rust tracks lifetimes to ensure that references never outlive the data they point to.

For most everyday code, the compiler infers lifetimes automatically through lifetime elision rules. You only need to annotate them explicitly in more complex situations (covered in the Lifetimes section).

The core rule: a reference's lifetime must be entirely contained within the owner's lifetime.

RUST
fn main() {
    let r;                   // r declared — no value yet

    {
        let x = 5;
        r = &x;              // r borrows x
        println!("{}", r);   // OK — x is still alive here
    }                        // x is dropped here

    // println!("{}", r);    // ERROR — r would outlive x!
}
error[E0597]: `x` does not live long enough
 --> src/main.rs:7:13
  |
6 |         r = &x;
  |             ^^ borrowed value does not live long enough
8 |     }
  |     - `x` dropped here while still borrowed
9 |
10|     println!("{}", r);
  |                    - borrow later used here
Before and After: Why Borrowing Matters

Without Borrowing

With Borrowing

Must return value to regain access

Original still accessible after function call

fn f(s: String) — takes ownership

fn f(s: &String) — borrows only

Caller uses tuple to get value back

Caller passes &s, keeps using s freely

Every use of a value involves moving

Multiple functions can read the same value

Code is verbose and awkward

Code is clean and natural

The Two Rules of References

Rust enforces exactly two rules about references at compile time:

  1. At any given time, you can have either any number of immutable references, or exactly one mutable reference — never both simultaneously.

  2. References must always be valid. A reference can never outlive the data it points to (no dangling references).

Note
Rule 1 prevents data races. Rule 2 prevents dangling references. Together, they eliminate entire classes of memory bugs without any runtime cost or garbage collector.
Quick Reference: Borrowing Syntax

Syntax

Meaning

&value

Create an immutable reference to value

&T

Type annotation for an immutable reference to T

*r

Dereference r to get the underlying value

&s[0..5]

Create a slice reference (elements 0 to 4)

&str

A string slice type (immutable reference to string data)

&[T]

A slice of type T (reference into an array or vec)

Summary
  • Borrowing lets you use a value without taking ownership of it

  • Create a reference with &value — this act is called borrowing

  • Immutable references (&T) provide read-only access to data

  • Multiple immutable references to the same value can coexist safely

  • References do not own data — the original owner is unaffected

  • Rust prevents dangling references at compile time — always

  • Slices (&str, &[T]) are reference types pointing into part of a collection

  • Every reference has a lifetime that must fit within the owner's lifetime