Bird
Raised Fist0
Rustprogramming~15 mins

Borrowing rules in Rust - Deep Dive

Choose your learning style10 modes available
Overview - Borrowing rules
What is it?
Borrowing rules in Rust are a set of guidelines that control how references to data are used. They ensure that you can have either one mutable reference or any number of immutable references to a value at a time, but never both. This helps prevent bugs like data races and dangling pointers by enforcing safe access to memory. Borrowing is a core part of Rust's safety without needing a garbage collector.
Why it matters
Without borrowing rules, programs can easily have bugs where data changes unexpectedly or memory is accessed after being freed. This leads to crashes or security problems. Borrowing rules let Rust catch these issues at compile time, so your programs are safer and more reliable. They allow you to write fast code without worrying about manual memory management errors.
Where it fits
Before learning borrowing rules, you should understand Rust's ownership system and basic references. After mastering borrowing, you can learn about lifetimes, smart pointers, and concurrency in Rust. Borrowing rules are foundational for safe Rust programming and come right after ownership in the learning path.
Mental Model
Core Idea
You can either have one person changing the data or many people reading it, but never both at the same time.
Think of it like...
Imagine a library book: either one person can borrow it to write notes inside (mutable), or many people can read it quietly without changing it (immutable), but not both at once.
┌───────────────────────────────┐
│           Data Value           │
├──────────────┬────────────────┤
│ Immutable    │ Mutable        │
│ References   │ Reference      │
│ (many allowed)│ (only one)     │
└──────────────┴────────────────┘
Rules:
- Multiple immutable refs allowed simultaneously
- Only one mutable ref allowed at a time
- Mutable and immutable refs cannot coexist
Build-Up - 7 Steps
1
FoundationUnderstanding Ownership Basics
🤔
Concept: Ownership means each value has one owner responsible for cleaning it up.
In Rust, every value has a single owner variable. When the owner goes out of scope, Rust automatically frees the value. This prevents memory leaks and dangling pointers. For example: let s = String::from("hello"); // s owns the string data // when s goes out of scope, data is freed
Result
The string data is automatically cleaned up when s is no longer used.
Understanding ownership is key because borrowing rules build on who owns the data and how others can access it safely.
2
FoundationIntroduction to References
🤔
Concept: References let you look at data without taking ownership.
You can create references to values using & for immutable references and &mut for mutable references. References let you use data without moving or copying it. Example: let s = String::from("hello"); let r = &s; // immutable reference println!("{}", r); // use without owning let mut s2 = String::from("hi"); let r2 = &mut s2; // mutable reference r2.push_str(" there");
Result
You can read or change data through references without owning it.
References allow safe sharing of data, but rules are needed to avoid conflicts when multiple references exist.
3
IntermediateImmutable Borrowing Rules
🤔Before reading on: do you think you can have multiple immutable references and a mutable reference at the same time? Commit to your answer.
Concept: You can have many immutable references to data simultaneously, but no mutable references at that time.
Rust allows multiple immutable references (&) to the same data at once. This is safe because none can change the data, so no conflicts occur. Example: let s = String::from("hello"); let r1 = &s; let r2 = &s; println!("{} and {}", r1, r2); // both read-only // But you cannot have a mutable reference while these exist.
Result
Multiple readers can safely access data without changing it.
Knowing that many readers can coexist without risk helps you design safe, concurrent-like access patterns.
4
IntermediateMutable Borrowing Rules
🤔Before reading on: do you think Rust allows two mutable references to the same data at once? Commit to your answer.
Concept: Only one mutable reference (&mut) to data is allowed at a time, and no immutable references can coexist.
Rust enforces that only one mutable reference exists at a time to prevent data races. This exclusive access means you can safely change data without interference. Example: let mut s = String::from("hello"); let r = &mut s; r.push_str(" world"); // No other references allowed while r exists.
Result
Exclusive write access prevents conflicting changes and ensures safety.
Understanding exclusive mutable access is crucial to avoid bugs from simultaneous writes.
5
IntermediateScope and Borrowing Lifetimes
🤔
Concept: References are valid only within certain scopes, preventing dangling references.
Rust tracks how long references live, called lifetimes. A reference cannot outlive the data it points to. For example: let r; { let s = String::from("hello"); r = &s; // r borrows s } // s goes out of scope here println!("{}", r); // error: r points to freed data Rust prevents this at compile time.
Result
References always point to valid data during their lifetime.
Knowing lifetimes prevents bugs where references point to invalid memory.
6
AdvancedBorrow Checker and Compile-Time Enforcement
🤔Before reading on: do you think Rust checks borrowing rules only at runtime or compile time? Commit to your answer.
Concept: Rust's borrow checker enforces borrowing rules during compilation, preventing unsafe code from running.
The borrow checker analyzes your code to ensure references follow the rules: no simultaneous mutable and immutable borrows, no dangling references, and proper lifetimes. If rules are broken, compilation fails with helpful errors. This means unsafe memory access is caught before the program runs.
Result
Unsafe borrowing mistakes are caught early, improving program safety.
Understanding compile-time checks explains why Rust programs are safe without runtime overhead.
7
ExpertInterior Mutability and Borrowing Exceptions
🤔Before reading on: do you think Rust's borrowing rules can be bypassed safely? Commit to your answer.
Concept: Rust allows controlled exceptions to borrowing rules using interior mutability patterns like RefCell and Mutex.
Sometimes you need to mutate data even when you only have immutable references. Rust provides types like RefCell that enforce borrowing rules at runtime instead of compile time. This allows mutable access inside immutable contexts safely, but with runtime checks that can panic if misused. Example: use std::cell::RefCell; let data = RefCell::new(5); *data.borrow_mut() += 1; This pattern is used carefully in complex scenarios like GUI programming or concurrency.
Result
Borrowing rules can be flexibly extended with runtime checks for advanced use cases.
Knowing interior mutability reveals how Rust balances safety with flexibility in real-world systems.
Under the Hood
Rust's compiler tracks ownership and references using a system called the borrow checker. It assigns lifetimes to references and enforces rules that prevent multiple mutable references or mutable and immutable references coexisting. This is done by analyzing the code's abstract syntax tree and control flow to ensure safe memory access without runtime cost. The compiler inserts no runtime checks for borrowing; all safety is guaranteed at compile time.
Why designed this way?
Rust was designed to provide memory safety without a garbage collector. Borrowing rules enforce safe access patterns that prevent common bugs like data races and dangling pointers. Alternatives like garbage collection add runtime overhead, while manual memory management is error-prone. Borrowing rules strike a balance by using compile-time checks to ensure safety and performance.
┌───────────────┐
│   Source      │
│   Code        │
└──────┬────────┘
       │
       ▼
┌───────────────┐
│ Borrow Checker│
│ (Compile-time)│
└──────┬────────┘
       │
       ▼
┌───────────────┐
│ Validated     │
│ References    │
│ & Ownership   │
└──────┬────────┘
       │
       ▼
┌───────────────┐
│ Safe Machine  │
│ Code (No      │
│ Runtime Checks)│
└───────────────┘
Myth Busters - 4 Common Misconceptions
Quick: Can you have both mutable and immutable references to the same data at the same time in Rust? Commit to yes or no.
Common Belief:You can have multiple immutable references and a mutable reference to the same data simultaneously if you are careful.
Tap to reveal reality
Reality:Rust strictly forbids having mutable and immutable references to the same data at the same time to prevent data races.
Why it matters:Ignoring this leads to undefined behavior and bugs that Rust's compiler prevents, so violating it breaks Rust's safety guarantees.
Quick: Does Rust allow mutable references to data even if the data is not declared mutable? Commit to yes or no.
Common Belief:You can create mutable references to any data regardless of whether the original variable is mutable.
Tap to reveal reality
Reality:Rust requires the original data to be declared mutable to create mutable references to it.
Why it matters:Trying to mutate immutable data causes compile errors, enforcing clear intent and preventing accidental changes.
Quick: Does Rust's borrowing rules only apply at runtime? Commit to yes or no.
Common Belief:Borrowing rules are checked during program execution to catch errors dynamically.
Tap to reveal reality
Reality:Borrowing rules are enforced at compile time by the borrow checker, preventing unsafe code from compiling.
Why it matters:This design avoids runtime overhead and catches bugs early, improving program safety and performance.
Quick: Can you safely bypass borrowing rules using unsafe code without any risk? Commit to yes or no.
Common Belief:Using unsafe code lets you ignore borrowing rules safely if you are careful.
Tap to reveal reality
Reality:Unsafe code can bypass borrowing rules but risks undefined behavior if misused; it requires careful manual guarantees.
Why it matters:Misusing unsafe code can cause serious bugs and security issues, so borrowing rules are critical for safe Rust.
Expert Zone
1
Mutable references guarantee exclusive access, but interior mutability types allow mutation through immutable references with runtime checks.
2
The borrow checker uses non-lexical lifetimes to allow more flexible borrowing scopes than simple block-based lifetimes.
3
Borrowing rules also prevent data races in concurrent code by enforcing exclusive mutable access or shared immutable access.
When NOT to use
Borrowing rules are fundamental in Rust, but when you need shared mutable state across threads, you should use synchronization primitives like Mutex or RwLock. For cases requiring mutation inside immutable contexts, use interior mutability types like RefCell or Cell. Unsafe code can bypass borrowing rules but should be avoided unless absolutely necessary.
Production Patterns
In real-world Rust, borrowing rules guide API design to expose safe interfaces. Libraries use lifetimes and borrowing to prevent misuse. Interior mutability is common in GUI frameworks and async runtimes. Concurrency uses borrowing rules combined with synchronization to avoid data races. Understanding borrowing deeply helps debug complex lifetime errors and optimize performance.
Connections
Ownership
Borrowing rules build directly on ownership principles.
Knowing ownership helps understand why borrowing rules exist to safely share or mutate owned data.
Concurrency Control
Borrowing rules enforce safe access patterns similar to locks in concurrency.
Understanding borrowing clarifies how Rust prevents data races without runtime locks in many cases.
Database Transaction Isolation
Borrowing rules resemble isolation levels that control read/write access to data.
Seeing borrowing as a form of access control helps understand how safe concurrent access is managed in different fields.
Common Pitfalls
#1Trying to create a mutable reference while immutable references exist.
Wrong approach:let s = String::from("hello"); let r1 = &s; let r2 = &s; let r3 = &mut s; // error: cannot borrow as mutable because immutable borrows exist println!("{} {} {}", r1, r2, r3);
Correct approach:let mut s = String::from("hello"); let r1 = &s; let r2 = &s; // r1 and r2 go out of scope here let r3 = &mut s; // allowed after immutable borrows end println!("{}", r3);
Root cause:Misunderstanding that mutable and immutable references cannot coexist simultaneously.
#2Attempting to mutate data through an immutable reference.
Wrong approach:let s = String::from("hello"); let r = &s; r.push_str(" world"); // error: cannot borrow immutable reference as mutable
Correct approach:let mut s = String::from("hello"); let r = &mut s; r.push_str(" world");
Root cause:Not declaring the original variable as mutable before creating mutable references.
#3Returning a reference to data that goes out of scope.
Wrong approach:fn get_ref() -> &String { let s = String::from("hello"); &s // error: returns reference to local variable }
Correct approach:fn get_string() -> String { let s = String::from("hello"); s // return ownership instead of reference }
Root cause:Not understanding lifetimes and that references must not outlive the data they point to.
Key Takeaways
Borrowing rules ensure safe access to data by allowing either multiple readers or one writer at a time, never both.
Rust enforces borrowing rules at compile time, preventing common bugs like data races and dangling pointers without runtime cost.
References have lifetimes that guarantee they never outlive the data they point to, avoiding invalid memory access.
Interior mutability patterns provide controlled exceptions to borrowing rules for advanced use cases with runtime checks.
Mastering borrowing rules is essential for writing safe, efficient, and concurrent Rust programs.

Practice

(1/5)
1. What does borrowing in Rust allow you to do?
easy
A. Copy data to a new variable
B. Use data without taking ownership
C. Delete data immediately
D. Create multiple owners of data

Solution

  1. Step 1: Understand borrowing concept

    Borrowing means using data without taking ownership, so the original owner keeps control.
  2. Step 2: Compare options

    Copying or deleting data changes ownership or data state, which borrowing does not do.
  3. Final Answer:

    Use data without taking ownership -> Option B
  4. Quick Check:

    Borrowing = Use without ownership [OK]
Hint: Borrowing means using data without owning it [OK]
Common Mistakes:
  • Confusing borrowing with copying
  • Thinking borrowing deletes data
  • Assuming borrowing creates multiple owners
2. Which of the following is the correct syntax to create an immutable borrow of a variable x in Rust?
easy
A. let y = &x;
B. let y = *x;
C. let y = &mut x;
D. let y = x;

Solution

  1. Step 1: Identify immutable borrow syntax

    Immutable borrow uses &x to create a reference without allowing changes.
  2. Step 2: Check other options

    &mut x is mutable borrow, *x dereferences, and x moves ownership.
  3. Final Answer:

    let y = &x; -> Option A
  4. Quick Check:

    Immutable borrow = &x [OK]
Hint: Immutable borrow uses & before variable [OK]
Common Mistakes:
  • Using &mut for immutable borrow
  • Dereferencing instead of borrowing
  • Assigning ownership instead of reference
3. What will be the output of this Rust code?
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
let r3 = &mut s;
println!("{}", r3);
medium
A. hello and hello\nerror at runtime
B. hello and hello\nhello
C. Compile error due to mutable borrow while immutable borrows exist
D. hello\nhello and hello

Solution

  1. Step 1: Analyze immutable borrows

    Two immutable borrows r1 and r2 are allowed simultaneously.
  2. Step 2: Check mutable borrow after immutable borrows

    Trying to create mutable borrow r3 while immutable borrows exist causes compile error.
  3. Final Answer:

    Compile error due to mutable borrow while immutable borrows exist -> Option C
  4. Quick Check:

    Mutable borrow disallowed with active immutable borrows [OK]
Hint: No mutable borrow allowed if immutable borrows exist [OK]
Common Mistakes:
  • Assuming mutable borrow works after immutable borrows
  • Expecting runtime error instead of compile error
  • Confusing output with allowed borrows
4. Identify the error in this Rust code snippet:
let mut data = vec![1, 2, 3];
let r1 = &mut data;
let r2 = &data;
r1.push(4);
println!("{:?}", r2);
medium
A. No error, code runs fine
B. Cannot push to vector inside mutable borrow
C. Cannot print vector with {:?}
D. Cannot borrow data as immutable while mutable borrow exists

Solution

  1. Step 1: Check borrows

    Variable r1 is a mutable borrow, r2 is an immutable borrow of the same data.
  2. Step 2: Understand borrowing rules

    Rust forbids immutable borrow while mutable borrow is active, causing a compile error.
  3. Final Answer:

    Cannot borrow data as immutable while mutable borrow exists -> Option D
  4. Quick Check:

    Immutable borrow disallowed during mutable borrow [OK]
Hint: No immutable borrow allowed during mutable borrow [OK]
Common Mistakes:
  • Thinking push is invalid inside mutable borrow
  • Ignoring borrow conflict between r1 and r2
  • Assuming printing with {:?} causes error
5. You want to write a function that takes a mutable reference to a vector and appends a number, but also returns the length of the vector after appending. Which function signature and usage follow Rust's borrowing rules correctly?
hard
A. fn add_and_len(v: &mut Vec<i32>, n: i32) -> usize { v.push(n); v.len() } let mut nums = vec![1,2]; let length = add_and_len(&mut nums, 3);
B. fn add_and_len(v: &Vec<i32>, n: i32) -> usize { v.push(n); v.len() } let mut nums = vec![1,2]; let length = add_and_len(&nums, 3);
C. fn add_and_len(v: Vec<i32>, n: i32) -> usize { v.push(n); v.len() } let nums = vec![1,2]; let length = add_and_len(nums, 3);
D. fn add_and_len(v: &mut Vec<i32>, n: i32) -> usize { v.len(); v.push(n) } let mut nums = vec![1,2]; let length = add_and_len(&mut nums, 3);

Solution

  1. Step 1: Check function signature for mutable borrow

    Function needs &mut Vec<i32> to modify vector by pushing a number.
  2. Step 2: Verify function body and usage

    fn add_and_len(v: &mut Vec<i32>, n: i32) -> usize { v.push(n); v.len() } let mut nums = vec![1,2]; let length = add_and_len(&mut nums, 3); pushes number then returns length correctly; usage passes mutable reference.
  3. Step 3: Analyze other options

    fn add_and_len(v: &Vec<i32>, n: i32) -> usize { v.push(n); v.len() } let mut nums = vec![1,2]; let length = add_and_len(&nums, 3); uses immutable borrow but tries to push (not allowed). fn add_and_len(v: Vec<i32>, n: i32) -> usize { v.push(n); v.len() } let nums = vec![1,2]; let length = add_and_len(nums, 3); takes ownership, not borrow. fn add_and_len(v: &mut Vec<i32>, n: i32) -> usize { v.len(); v.push(n) } let mut nums = vec![1,2]; let length = add_and_len(&mut nums, 3); returns length before push, so length is old value.
  4. Final Answer:

    fn add_and_len(v: &mut Vec<i32>, n: i32) -> usize { v.push(n); v.len() } let mut nums = vec![1,2]; let length = add_and_len(&mut nums, 3); -> Option A
  5. Quick Check:

    Mutable borrow needed to modify vector [OK]
Hint: Use &mut to modify data inside function [OK]
Common Mistakes:
  • Using immutable borrow when modifying data
  • Taking ownership instead of borrowing
  • Returning length before modification
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard