KAIROS CODERS

Rust Borrowing & References: &, &mut, Dereferencing and the Borrow Checker

user

Rahul

September 02, 2026 at 06:59 PM

View Count: 14

Rust Borrowing & References: &, &mut, Dereferencing and the Borrow Checker

In the previous article, we learned the most important idea in Rust:

Ownership.

We learned that a value can have one owner, ownership can move, and values are automatically cleaned up when their owner goes out of scope.

But ownership alone would make Rust unnecessarily difficult to use.

Imagine you have a String and want to let a function read it without taking ownership.

You don't want this:

 

fn print_name(name: String) {
    println!("{name}");
}

 

because passing the String moves ownership.

Instead, Rust gives us borrowing.

 

fn print_name(name: &String) {
    println!("{name}");
}

 

Now the function can use the value without owning it.

This article is where the borrow checker starts to become understandable.

We'll learn:

  • References
  • Immutable borrowing
  • Mutable borrowing
  • &
  • &mut
  • Dereferencing with *
  • Borrowing rules
  • Multiple immutable references
  • Mutable reference restrictions
  • Reference scope
  • Non-Lexical Lifetimes
  • String slices
  • Array slices
  • String vs &str
  • Borrowing in functions
  • Common borrow-checker errors
  • Practical examples

What Is Borrowing?

Borrowing means:

Using a value without taking ownership of it.

Suppose:

 

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

    print_name(&name);

    println!("{name}");
}

fn print_name(name: &String) {
    println!("{name}");
}

 

The important part is:

 

print_name(&name);

 

The & creates a reference.

The function receives a reference instead of receiving ownership of the String.

Conceptually:

main
 │
 │ owns
 ▼
String
 │
 │ borrowed
 ▼
print_name()

 

After print_name() finishes, the original owner still owns the String.


What Is a Reference?

A reference is a way to refer to a value without owning it.

Example:

 

let name = String::from("Rust");

let reference = &name;

 

Now:

name
 │
 ▼
String "Rust"

reference
 │
 └──────→ borrows name

 

The reference doesn't own the String.


The & Operator

The & symbol creates a reference.

 

let number = 100;

let reference = &number;

 

You can think of:

 

&number

 

as:

"Give me a reference to number."


Reading Through a Reference

You can generally use a reference just like the value it refers to.

 

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

    let reference = &name;

    println!("{reference}");
}

 

Rust automatically handles the appropriate dereferencing in many normal expressions.

This is called deref coercion/autoderef behavior, which we'll explore more later.


Ownership vs Borrowing

Compare the two approaches.

Ownership

 

fn consume(name: String) {
    println!("{name}");
}

 

Calling:

 

consume(name);

 

moves ownership.

Borrowing

 

fn read(name: &String) {
    println!("{name}");
}

 

Calling:

 

read(&name);

 

borrows the value.

The difference:

Ownership:

main ─────→ function
      ownership moves


Borrowing:

main ─────→ value
      ↑
      │
   function borrows

 


Immutable References

The simplest reference is an immutable reference.

 

let name = String::from("Rust");

let reference = &name;

 

You can read through it:

 

println!("{reference}");

 

But you cannot modify the original value through an immutable reference.


Multiple Immutable References

Rust allows multiple immutable references at the same time.

 

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

    let first = &name;
    let second = &name;
    let third = &name;

    println!("{first}");
    println!("{second}");
    println!("{third}");
}

 

This is perfectly valid.

Why?

Because nobody is modifying the value.

Conceptually:

             ┌──→ first
             │
String ←─────┼──→ second
             │
             └──→ third

Everyone is reading.
Nobody is changing.

 

This is safe.


Mutable References

What if you want to modify a value through a reference?

Use:

 

&mut

 

Example:

 

fn add_text(text: &mut String) {
    text.push_str(" Programming");
}

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

    add_text(&mut message);

    println!("{message}");
}

 

Output:

Rust Programming

 

There are two important keywords here:

 

let mut message

 

and:

 

&mut message

 

Both matter.


Why Does the Original Variable Need mut?

This won't work:

 

let message = String::from("Rust");

add_text(&mut message);

 

because message itself isn't mutable.

You need:

 

let mut message = String::from("Rust");

 

Then:

 

add_text(&mut message);

 

is allowed.

Think:

mut variable
      ↓
can be modified

&mut variable
      ↓
create mutable reference

 


Mutable Reference Rules

Rust has an important rule:

At a given time, you can have either one mutable reference or any number of immutable references, but not both.

In simplified form:

Many readers
      OR
One writer

 

Not:

Many readers + writer

 

This prevents data races.


Example: Multiple Mutable References

This isn't allowed:

 

let mut value = 10;

let first = &mut value;
let second = &mut value;

 

Why?

There would be two active mutable references to the same value.

Rust prevents this situation.


Why Is This Restriction Necessary?

Imagine two pieces of code simultaneously modifying the same memory:

Thread A ──→ value
              ↑
Thread B ─────┘

 

Both could attempt to modify it at the same time.

The result could become unpredictable.

Rust's borrowing rules prevent many such problems at compile time.


Mutable + Immutable Reference

This is also problematic while the references are simultaneously active:

 

let mut value = 10;

let read = &value;
let write = &mut value;

 

Rust rejects this if the immutable borrow is still in use.

The problem is:

read  → reading
write → modifying

 

You don't want someone changing a value while another active reference relies on it remaining unchanged.


The Golden Borrowing Rule

Remember this:

                    ┌── Multiple immutable references
                    │
Borrowing ──────────┤
                    │
                    └── OR one mutable reference

 

Never:

Multiple immutable references
            +
Mutable reference

 

when their active lifetimes overlap.


A Very Important Detail: Scope

Consider:

 

fn main() {
    let mut value = 10;

    {
        let reference = &mut value;

        *reference += 10;
    }

    println!("{value}");
}

 

The mutable reference exists only inside the inner block.

When the block ends:

reference disappears
        ↓
value available again

 

Output:

20

 


Non-Lexical Lifetimes

Modern Rust has a very useful feature called Non-Lexical Lifetimes, commonly abbreviated NLL.

Consider:

 

fn main() {
    let mut value = 10;

    let reference = &value;

    println!("{reference}");

    let mutable_reference = &mut value;

    *mutable_reference += 10;

    println!("{value}");
}

 

This can work because the immutable reference is no longer used after:

 

println!("{reference}");

 

The compiler can determine that its borrow has ended before the mutable borrow begins.

Conceptually:

reference created
      ↓
reference used
      ↓
reference no longer needed
      ↓
mutable borrow allowed

 

This makes Rust's borrowing system more flexible than a simplistic "block ends, borrow ends" model.


Dereferencing

Now let's look at:

 

*

 

The * operator can be used to dereference a reference.

Example:

 

fn main() {
    let value = 10;

    let reference = &value;

    println!("{}", *reference);
}

 

Here:

 

reference

 

is a reference to the integer.

And:

 

*reference

 

accesses the value being referenced.


Visualizing Dereferencing

Think of:

value = 10

reference
    │
    ▼
  value
   10

 

Then:

*reference

 

means:

Follow the reference and access the underlying value.


Dereferencing a Mutable Reference

This becomes particularly useful when modifying values.

 

fn increase(value: &mut i32) {
    *value += 1;
}

fn main() {
    let mut number = 10;

    increase(&mut number);

    println!("{number}");
}

 

Output:

11

 

The expression:

 

*value += 1;

 

means:

Access the value being referenced and modify it.


Why Can We Usually Skip *?

You might wonder why this works:

 

fn print_name(name: &String) {
    println!("{name}");
}

 

instead of:

 

println!("{}", *name);

 

Rust provides automatic dereferencing in many contexts.

For example, method calls such as:

 

name.len()

 

can work even though name is a reference.

This is one of the conveniences provided by Rust's type system.


References Don't Own Values

This is extremely important.

Consider:

 

let name = String::from("Rust");

let reference = &name;

 

reference does not own the String.

Therefore, when the original owner goes out of scope:

name
 ↓
owns String

reference
 ↓
borrows String

 

the reference cannot keep the String alive by itself.

This prevents references from becoming hidden owners.


Dangling References

A dangling reference would point to memory that is no longer valid.

Imagine:

reference
    ↓
freed memory

 

That would be dangerous.

Rust's compiler prevents safe Rust code from creating ordinary dangling references.

For example, this doesn't compile:

 

fn create_reference() -> &String {
    let value = String::from("Rust");

    &value
}

 

Why?

Because value will be destroyed when the function ends.

Returning a reference to it would leave a reference pointing to invalid memory.

Rust catches this at compile time.


Why This Is Powerful

Languages with manual memory management can allow dangerous situations if the programmer isn't careful.

Rust instead asks:

Does this reference remain valid for as long as it is used?

If the answer is no, compilation fails.

This is one of the biggest advantages of Rust's ownership and borrowing model.


Borrowing String

Let's create a reusable function:

 

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

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

    print_length(&text);

    println!("Still available: {text}");
}

 

Output:

Length: 16
Still available: Rust Programming

 

The function borrowed the string.


Prefer &str for Read-Only String Parameters

In idiomatic Rust, you will often see:

 

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

 

instead of:

 

fn print_length(text: &String) {
}

 

Why?

Because &str is more flexible.

You can pass:

 

print_length("Hello");

 

and:

 

let message = String::from("Hello");

print_length(&message);

 

The function doesn't need to care whether the caller has a String or a string literal.


String vs &str

This distinction is extremely important.

String

An owned, growable string.

 

let message = String::from("Hello");

 

&str

A borrowed string slice.

 

let message = "Hello";

 

The second value is a string literal with type &'static str.

Conceptually:

String
 ↓
owns string data

&str
 ↓
borrows string data

 

We'll go deeper into this when we study slices and lifetimes.


String Slices

A slice is a reference to part of a collection.

Example:

 

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

    let part = &text[0..5];

    println!("{part}");
}

 

Output:

Hello

 

Here:

 

&text[0..5]

 

creates a string slice.


Understanding the Slice

Suppose:

Hello Rust
0123456789

 

Then:

 

&text[0..5]

 

represents:

Hello

 

It doesn't create a completely independent String.

It references part of the existing string.


Why Slices Are Useful

Suppose you need only part of a large string.

Instead of copying:

Entire String
      ↓
copy "some part"

 

you can borrow a slice:

Entire String
      ↑
      │
    &str

 

This can be efficient because the slice doesn't need to own another copy of the text.


Important: String Slicing Uses Byte Indices

Rust strings are UTF-8 encoded.

Therefore:

 

let text = String::from("Hello");
let part = &text[0..5];

 

works because these indices fall on valid UTF-8 character boundaries.

But arbitrary byte ranges can cause a runtime panic if they split a UTF-8 character.

For example, don't assume one character always equals one byte.

This is an important difference between Rust strings and arrays of simple bytes.


Array Slices

Slices aren't limited to strings.

Consider:

 

fn main() {
    let numbers = [10, 20, 30, 40, 50];

    let slice = &numbers[1..4];

    println!("{slice:?}");
}

 

Output:

[20, 30, 40]

 

The slice borrows part of the array.


Slice Type

For an array:

 

let numbers = [10, 20, 30];

 

a slice looks like:

 

&numbers[..]

 

Its type is:

&[i32]

 

You can think of:

&[T]

 

as:

A borrowed view into a sequence of T values.


Function Accepting a Slice

Instead of requiring an array of one exact size:

 

fn calculate(numbers: &[i32]) {
    // ...
}

 

the function can accept slices of different lengths.

Example:

 

fn print_numbers(numbers: &[i32]) {
    for number in numbers {
        println!("{number}");
    }
}

fn main() {
    let numbers = [10, 20, 30, 40];

    print_numbers(&numbers);
}

 

This is a very common Rust pattern.


&[T] Is Extremely Useful

You can use:

 

fn sum(numbers: &[i32]) -> i32 {
    let mut total = 0;

    for number in numbers {
        total += number;
    }

    total
}

 

Then:

 

fn main() {
    let numbers = [10, 20, 30, 40];

    println!("{}", sum(&numbers));
}

 

The function doesn't need to own the array.


Mutable Slices

You can also borrow a slice mutably.

 

fn double(numbers: &mut [i32]) {
    for number in numbers {
        *number *= 2;
    }
}

fn main() {
    let mut numbers = [1, 2, 3, 4];

    double(&mut numbers);

    println!("{numbers:?}");
}

 

Output:

[2, 4, 6, 8]

 

This combines:

  • Mutable borrowing
  • Slices
  • for
  • Dereferencing

Borrowing and Functions

A good function often takes the least ownership it needs.

Suppose a function only needs to read data.

Instead of:

 

fn calculate(data: Vec<i32>) {
}

 

consider:

 

fn calculate(data: &[i32]) {
}

 

The second function doesn't need ownership of the vector.

This makes the API more flexible.


Borrowing a Vec

Suppose:

 

let numbers = vec![10, 20, 30];

 

You can pass:

 

calculate(&numbers);

 

Rust can use the vector as a slice where appropriate.

For example:

 

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

fn main() {
    let numbers = vec![10, 20, 30];

    let result = sum(&numbers);

    println!("{result}");
}

 

Output:

60

 


The Borrow Checker

Now let's talk about the famous Rust borrow checker.

The borrow checker is part of the Rust compiler that checks whether references follow Rust's ownership and borrowing rules.

It asks questions such as:

  • Who owns this value?
  • Is the value still alive?
  • Is this reference valid?
  • Are there conflicting borrows?
  • Is someone trying to modify a value while it is immutably borrowed?
  • Could this reference outlive the value it refers to?

If your program violates the rules, compilation fails.


Think of the Borrow Checker as a Safety Inspector

Imagine:

Your Rust Code
      ↓
Compiler
      ↓
Ownership check
      ↓
Borrow check
      ↓
Type check
      ↓
Machine code

 

The compiler prevents many memory bugs before your program runs.

This is why Rust can be strict.

The compiler is essentially saying:

"Prove to me that this memory usage is safe."


A Classic Borrow Checker Example

Consider:

 

let mut value = 10;

let first = &value;
let second = &mut value;

println!("{first}");

 

This is problematic because the immutable reference is used while a mutable borrow exists.

Rust rejects it.


Fixing the Problem

One solution is to finish using the immutable reference first:

 

let mut value = 10;

let first = &value;

println!("{first}");

let second = &mut value;

*second += 1;

 

Now the immutable borrow is no longer needed before the mutable borrow begins.


Another Example

This is valid:

 

let mut value = 10;

{
    let reference = &mut value;

    *reference += 5;
}

println!("{value}");

 

The mutable reference's scope ends before value is used again.


Borrowing Rules Cheat Sheet

Keep this nearby while learning Rust.

Rule 1

You can have:

Any number of immutable references

 

at the same time.

Rule 2

You can have:

One mutable reference

 

at a time.

Rule 3

You cannot have active immutable and mutable references to the same value simultaneously.

Rule 4

References must never outlive the value they refer to.

These rules are the foundation of Rust's borrowing system.


A Simple Mental Model

Think of a value like a document.

Immutable borrow

Multiple people can read it:

Document
 ↑ ↑ ↑
 A B C

 

Mutable borrow

One person gets editing access:

Document
   ↑
 Editor

 

You don't want:

Document
 ↑ ↑
Reader Editor

 

at the same time if the reader relies on the document staying unchanged.

Rust enforces this concept at compile time.


Practical Example: Bank Account

Let's use borrowing in a realistic example.

 

struct Account {
    balance: f64,
}

fn show_balance(account: &Account) {
    println!("Balance: ₹{}", account.balance);
}

fn deposit(account: &mut Account, amount: f64) {
    account.balance += amount;
}

fn main() {
    let mut account = Account {
        balance: 1000.0,
    };

    show_balance(&account);

    deposit(&mut account, 500.0);

    show_balance(&account);
}

 

Output:

Balance: ₹1000
Balance: ₹1500

 

Notice the design:

show_balance()
       ↓
immutable borrow

deposit()
       ↓
mutable borrow

 

Each function gets exactly the access it needs.


Practical Example: Search in a Collection

 

fn contains(numbers: &[i32], target: i32) -> bool {
    for number in numbers {
        if *number == target {
            return true;
        }
    }

    false
}

fn main() {
    let numbers = [10, 20, 30, 40];

    println!("{}", contains(&numbers, 30));
}

 

Output:

true

 

The function doesn't take ownership of the array.

It simply borrows a slice.


Practical Example: Modify a Collection

 

fn increase_all(numbers: &mut [i32]) {
    for number in numbers {
        *number += 10;
    }
}

fn main() {
    let mut numbers = [1, 2, 3];

    increase_all(&mut numbers);

    println!("{numbers:?}");
}

 

Output:

[11, 12, 13]

 


Why Borrowing Matters for Performance

Suppose you have a large vector:

 

let numbers = vec![/* millions of values */];

 

You don't want every function call to copy the entire vector.

Instead:

 

fn process(numbers: &[i32]) {
}

 

allows the function to operate on the existing data.

Conceptually:

Large collection
      ↓
Borrowed slice
      ↓
Function

 

No unnecessary ownership transfer or data duplication.


Borrowing Is One of Rust's Superpowers

At this point, you can see how Rust combines:

Ownership
    +
Borrowing
    +
References
    +
Compile-time checking

 

to achieve memory safety without requiring a garbage collector.

This is one of the reasons Rust is widely used for:

  • Operating systems
  • Databases
  • Game engines
  • Networking
  • Embedded systems
  • WebAssembly
  • High-performance backend services
  • CLI applications
  • Infrastructure software

Common Beginner Mistakes

Mistake 1 — Forgetting &

Suppose:

 

fn print_name(name: &String) {
    println!("{name}");
}

 

You need:

 

print_name(&name);

 

not:

 

print_name(name);

 

because the latter attempts to pass ownership.


Mistake 2 — Forgetting mut

This:

 

let value = 10;

 

cannot be mutably borrowed.

Use:

 

let mut value = 10;

 

then:

 

let reference = &mut value;

 


Mistake 3 — Creating Conflicting Borrows

Avoid:

 

let read = &value;
let write = &mut value;

 

when both references are simultaneously active.


Mistake 4 — Returning a Reference to a Local Variable

This is invalid:

 

fn create() -> &String {
    let value = String::from("Rust");

    &value
}

 

The local value disappears when the function returns.


Mistake 5 — Using String When &str Is Better

For read-only string parameters, prefer:

 

fn process(text: &str) {
}

 

when appropriate.


Practice Exercises

Exercise 1 — Borrowing

Create:

 

fn print_number(number: &i32)

 

and call it without transferring ownership.


Exercise 2 — Mutable Borrow

Create:

 

fn increment(number: &mut i32)

 

that increases a number by 1.


Exercise 3 — String Modification

Create:

 

fn add_exclamation(text: &mut String)

 

that turns:

Hello

 

into:

Hello!

 


Exercise 4 — Slice

Given:

 

let numbers = [10, 20, 30, 40, 50];

 

create a slice containing:

20, 30, 40

 


Exercise 5 — Sum a Slice

Write:

 

fn sum(numbers: &[i32]) -> i32

 

that returns the sum.


Exercise 6 — Find Maximum

Write:

 

fn maximum(numbers: &[i32]) -> i32

 

that returns the largest value.


Exercise 7 — Borrow Checker

Experiment with:

 

let mut value = 10;

let a = &value;
let b = &value;

println!("{a}");
println!("{b}");

 

Then try:

 

let mut value = 10;

let a = &value;
let b = &mut value;

println!("{a}");

 

Observe what the compiler tells you.


Ownership → Borrowing → Lifetimes

You've now reached an important point in the Rust learning path.

The progression looks like this:

Variables
    ↓
Data Types
    ↓
Control Flow
    ↓
Functions
    ↓
Ownership
    ↓
Borrowing
    ↓
References
    ↓
Lifetimes

 

And lifetimes are the next major piece.

Pixels to Perfection Design that Impresses

Want to partner with us? let's innovate together