Welcome back, intrepid Rustacean! In Chapter 4, we took our first exciting dive into Rust’s unique ownership system. We learned that every piece of data in Rust has a single “owner,” and when that owner goes out of scope, the data is automatically cleaned up. This powerful concept prevents many common memory bugs, but it also means we can’t just pass data around willy-nilly without giving up ownership. As of Rust 1.94.0 (stable release checked 2026-03-20), these core memory safety principles remain fundamental.
So, what if you want to let a function use some data without taking ownership of it? What if you need to share data between different parts of your program without constantly copying it or transferring ownership? That’s where the twin superpowers of Borrowing and Lifetimes come into play!
In this chapter, we’ll unlock the secrets of how Rust allows you to create references to data, enabling flexible and efficient data sharing while still upholding its strict memory safety guarantees. We’ll explore the rules of borrowing, understand why lifetimes are essential, and learn how to wield them effectively. This is often considered one of the trickiest parts of learning Rust, but with our step-by-step approach, you’ll gain a solid understanding and feel much more confident in writing robust Rust code.
Ready to conquer the borrow checker? Let’s dive in!
Recap: Ownership in a Nutshell
Before we jump into borrowing, let’s quickly refresh our memory on ownership. Remember these three golden rules from Chapter 4:
- Each value in Rust has a variable that’s its owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value will be dropped.
This system ensures memory safety at compile time, meaning you won’t have dangling pointers or double-frees at runtime. But it also means that if you pass a String to a function, that function takes ownership, and you can’t use the String anymore in the original scope.
fn takes_ownership(some_string: String) {
println!("I own: {}", some_string);
} // some_string goes out of scope and `drop` is called.
fn main() {
let s1 = String::from("hello");
takes_ownership(s1);
// println!("{}", s1); // ๐ ERROR! s1 is now invalid, ownership moved!
}
If you tried to uncomment println!("{}", s1);, the Rust compiler would gracefully inform you: “value borrowed here after move”. This is precisely the kind of safety guarantee we appreciate!
Introducing References: Borrowing Data
Imagine you have a valuable book. You want a friend to read it, but you don’t want to give them the book permanently. Instead, you lend it to them. They can read it, but they know they need to return it to you. In Rust, borrowing is like lending your data. You give a function or a block of code a reference to your data, allowing it to use the data without taking ownership.
References are denoted by the & symbol. When you pass a reference to a function, we say you are “borrowing” the value.
Let’s modify our previous example to use references:
// This function takes a reference to a String, not ownership.
// It "borrows" the String.
fn uses_reference(some_string: &String) {
println!("I'm borrowing: {}", some_string);
} // some_string (the reference) goes out of scope, but the original data is NOT dropped.
fn main() {
let s1 = String::from("hello, Rust!");
uses_reference(&s1); // We pass a reference to s1.
println!("I still own s1: {}", s1); // ๐ s1 is still valid here!
}
What happened here?
When we call uses_reference(&s1), we’re creating a reference to s1 and passing that reference to the function. The function uses_reference receives some_string as &String, which means it can read the String data, but it cannot modify it or take ownership. When uses_reference finishes, the reference some_string goes out of scope, but s1 itself remains owned by main and is perfectly valid for further use. This is much more flexible!
Mutable vs. Immutable References
Just like variables can be mutable or immutable, so can references.
Immutable References (
&T): These allow you to read data but not modify it. Theuses_referencefunction above took an immutable reference. You can have multiple immutable references to the same data at the same time. Think of it as multiple people reading the same book simultaneously.Mutable References (
&mut T): These allow you to read and modify data. To create a mutable reference, both the original variable and the reference itself must be declared as mutable.
Let’s see an example with mutable references:
fn change_value(some_integer: &mut i32) {
*some_integer += 10; // Dereference the mutable reference to modify the value
println!("Value inside function: {}", some_integer);
}
fn main() {
let mut my_number = 5; // my_number must be mutable for a mutable reference
println!("Original number: {}", my_number); // Output: Original number: 5
change_value(&mut my_number); // Pass a mutable reference to my_number
println!("Number after change: {}", my_number); // Output: Number after change: 15
}
Key Observation: Notice the * before some_integer in *some_integer += 10;. This is called dereferencing. It means “go to the value this reference points to.” Without the *, some_integer would refer to the reference itself, not the underlying integer value.
The Golden Rule of References: “One Writer or Many Readers”
This is THE most important rule of Rust’s borrowing system, enforced by the borrow checker. It’s what ensures memory safety when using references:
At any given time, you can have either:
- One mutable reference to a particular piece of data. (One person can edit the book at a time.)
- Any number of immutable references to that data. (Many people can read the book at the same time.)
You CANNOT have both a mutable reference and any other references (mutable or immutable) to the same data in the same scope at the same time.
This rule prevents data races at compile time, a common and hard-to-debug problem in other languages. A data race occurs when:
- Two or more pointers access the same data at the same time.
- At least one of the pointers is writing to the data.
- There’s no mechanism to synchronize access to the data.
Let’s illustrate the “One Writer or Many Readers” rule with code:
fn main() {
let mut s = String::from("hello");
let r1 = &s; // Immutable reference
let r2 = &s; // Another immutable reference
println!("r1: {}, r2: {}", r1, r2); // This is fine! Many readers.
// Try to uncomment the next line and observe the error!
// let r3 = &mut s; // ๐ ERROR! Cannot borrow `s` as mutable because it is also borrowed as immutable.
// println!("{}", r3);
// After r1 and r2 are no longer used, we can create a mutable reference:
// The borrow checker is smart enough to know that r1 and r2 are no longer used after the println!
let r3 = &mut s; // This is fine, as r1 and r2's "active" period has ended.
r3.push_str(", world!");
println!("{}", r3); // Output: hello, world!
// What if we try to use r1 after r3 is created and used?
// Try to uncomment the next line and observe the error!
// println!("r1: {}", r1); // ๐ ERROR! r1's borrow would extend past the creation of r3.
}
The borrow checker is incredibly smart. It knows when a reference is last used. In the example above, r1 and r2 are considered “active” until their last use (println!). Once they are no longer used, their borrows “end,” and a new mutable borrow can be created. This concept of when a reference is active is crucial and leads us directly into lifetimes.
Here’s a simplified flow of the borrow checker’s logic:
Mini-Challenge: Try to write a small program where you intentionally violate the “One Writer or Many Readers” rule. For example, create an immutable reference, then a mutable one, and then try to use the immutable one again. Observe the error messages the Rust compiler provides. They are often very helpful in guiding you to fix the issue!
Understanding Lifetimes: Ensuring References Are Valid
The borrow checker uses lifetimes to ensure that all references are valid for as long as they are used. In simple terms, a lifetime is the scope for which a reference is valid. Every reference in Rust has a lifetime, even if you don’t explicitly write it.
Why are lifetimes necessary? The Dangling Reference Problem
Consider this common problem in languages without Rust’s safety guarantees:
int* dangling_pointer() {
int x = 10;
return &x; // Returns a pointer to a local variable x
} // x is destroyed here! Memory for x is deallocated.
int main() {
int* ptr = dangling_pointer();
// *ptr is now a dangling pointer, pointing to invalid memory!
// Dereferencing it leads to undefined behavior, crashes, or security vulnerabilities.
printf("%d\n", *ptr);
return 0;
}
This is a dangling pointer โ a pointer that refers to memory that has been deallocated. Rust’s ownership and borrowing rules, combined with lifetimes, prevent this exact scenario at compile time.
How Rust Prevents Dangling References with Lifetimes
Rust ensures that a reference never outlives the data it points to. The compiler analyzes the scopes of variables and references to make sure this rule is upheld.
Let’s look at a Rust example that would create a dangling reference if not for the borrow checker:
// This code will NOT compile!
// fn dangle() -> &String { // ๐ ERROR: expected named lifetime parameter
// let s = String::from("hello"); // s is created here
// &s // We return a reference to s
// } // s goes out of scope here, and is dropped! The reference would be dangling.
// fn main() {
// let reference = dangle();
// }
If you uncomment this code, the Rust compiler will immediately tell you: “this function’s return type contains a borrowed value, but there is no value for it to be borrowed from.” It means the data s lives only within the dangle function, but you’re trying to return a reference to it, which would then point to invalid memory once s is dropped. The compiler prevents this by checking the lifetimes.
Lifetime Elision: When Rust Infers Lifetimes
Often, you don’t need to explicitly write lifetimes. The Rust compiler has a set of rules called lifetime elision rules that allow it to infer lifetimes for common patterns. This makes Rust code much cleaner and less verbose.
For example, in a function that takes one reference and returns nothing:
fn print_greeting(greeting: &String) { // &String is really `&'a String`
println!("Hello, {}", greeting);
}
Rust automatically assigns a lifetime 'a to greeting. It knows that the reference greeting must be valid for the duration of the print_greeting function. Most simple functions fall under these rules, so you rarely need to annotate them.
Explicit Lifetimes in Functions
Sometimes, the compiler can’t infer lifetimes, especially when a function takes multiple references and needs to return one of them, or when it needs to ensure that the input references have a certain relationship to each other. In these cases, you need to explicitly annotate lifetimes.
Lifetime annotations don’t change how long a reference lives; they only describe the relationships between the lifetimes of multiple references.
Let’s take the classic example of a function that returns the longest of two string slices:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz"; // String literals have a 'static lifetime, but can be coerced to shorter ones.
let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);
// Another scenario to test lifetime rules:
let string3 = String::from("long string is long");
let result2; // Declare result2 outside the inner scope
{
let string4 = String::from("xyz");
result2 = longest(string3.as_str(), string4.as_str());
// If string4 were the longest, result2's lifetime would be tied to string4.
// But in this case, string3 is longer, so result2 borrows from string3.
println!("The longest string from inner scope is {}", result2);
} // string4 goes out of scope here.
// If result2 had borrowed from string4, this line would be an error.
// Because string3 is longer and lives longer, result2 is still valid.
// The lifetime of result2 is tied to the *shorter* of the two input lifetimes,
// which the compiler determines to be string3's lifetime (which extends past this block).
println!("Result2 is still valid after string4 dropped: {}", result2);
}
Let’s break down fn longest<'a>(x: &'a str, y: &'a str) -> &'a str:
<'a>: This declares a generic lifetime parameter named'a. It’s convention to use lowercase, single quotes, and short names for lifetimes.x: &'a strandy: &'a str: These mean thatxandyare string slices that must both live at least as long as the lifetime'a.-> &'a str: This means the reference returned bylongestwill also live at least as long as'a.
What does this achieve?
This signature tells the compiler: “The returned reference will be valid for the smallest lifetime of the two input references.” The compiler then ensures that whatever result is assigned to will not outlive the shortest-lived input. In our second main scenario, if string4 were chosen as the longest, and we tried to use result2 outside that inner scope, the compiler would prevent it because string4 would have been dropped. This is the magic of lifetimes at work!
Struct Lifetimes
If a struct holds a reference, you must specify a lifetime parameter for that reference. This tells the compiler that instances of the struct cannot outlive the data they refer to.
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
let i = ImportantExcerpt {
part: first_sentence,
};
println!("Important part: {}", i.part);
// If 'novel' went out of scope before 'i', the compiler would catch it!
// Let's try to create a scenario that would be an error if allowed:
// Try to uncomment this block and see the compiler error!
// let broken_excerpt;
// {
// let temp_string = String::from("temporary data");
// broken_excerpt = ImportantExcerpt { part: temp_string.as_str() };
// } // temp_string dropped here! broken_excerpt.part would be a dangling reference!
// println!("{}", broken_excerpt.part); // ๐ ERROR: `temp_string` does not live long enough
}
Here, ImportantExcerpt has a lifetime parameter 'a. This means that an ImportantExcerpt instance cannot outlive the reference it holds (part). The compiler ensures that the data novel lives at least as long as i.
The 'static Lifetime
There’s a special lifetime called 'static. It means that the data lives for the entire duration of the program. String literals ("hello world") have the 'static lifetime because they are stored directly in the program’s binary and are always available.
let s: &'static str = "I live for the entire program!";
println!("{}", s);
You’ll encounter 'static when dealing with global constants, embedded string literals, or certain types that truly exist for the program’s entire runtime. It’s the longest possible lifetime.
Step-by-Step Implementation: Building a Simple Reference Manager
Let’s create a small program that demonstrates borrowing and lifetimes by managing a collection of tasks. We’ll create a Task struct that holds a reference to a task description. This will solidify your understanding of how lifetimes are applied in practical scenarios.
First, create a new Rust project:
cargo new task_manager
cd task_manager
Now, open src/main.rs and let’s start coding!
Step 1: Define the Task struct with a lifetime parameter.
We want our Task to refer to a description, not own it. This means the Task struct needs a lifetime parameter to ensure the description it refers to outlives the Task itself.
Add this to src/main.rs:
// src/main.rs
// We need a lifetime parameter 'a because Task holds a reference (&'a str).
// This tells the compiler that any Task instance cannot outlive the
// string slice it's borrowing.
struct Task<'a> {
description: &'a str,
completed: bool,
}
impl<'a> Task<'a> {
// The 'new' function also needs the lifetime parameter.
// The description input reference must have the same lifetime 'a as the struct.
fn new(description: &'a str) -> Self {
Task {
description,
completed: false,
}
}
// This method takes a mutable reference to self, allowing us to modify the task.
fn mark_completed(&mut self) {
self.completed = true;
println!("Task '{}' marked as completed!", self.description);
}
// This method takes an immutable reference to self, allowing us to read task data.
fn display(&self) {
let status = if self.completed { "โ" } else { " " };
println!("[{}] {}", status, self.description);
}
}
fn main() {
// We'll put our main logic here soon!
println!("Task Manager is starting...");
}
Explanation:
struct Task<'a>: We declare a generic lifetime parameter'afor ourTaskstruct.description: &'a str: This field is a string slice with the lifetime'a. It means the data thatdescriptionpoints to must live at least as long as theTaskinstance itself.impl<'a> Task<'a>: When implementing methods for a struct with a lifetime parameter, we need to repeat the lifetime declaration.fn new(description: &'a str) -> Self: Thenewfunction takes a string slice, and we ensure its lifetime matches the struct’s lifetime. This is crucial for safety.fn mark_completed(&mut self): Takes a mutable reference toself(&mut Task). This allows us to modify thecompletedfield.fn display(&self): Takes an immutable reference toself(&Task). This allows us to readdescriptionandcompleted.
Step 2: Create and manage tasks in main.
Now, let’s create some tasks and interact with them in our main function. We’ll use a Vec<Task> to hold our tasks.
Replace the main function with this:
// ... (previous code for Task struct and impl) ...
fn main() {
println!("--- Simple Task Manager ---");
// The 'master_task_list' string owns the actual task descriptions.
// All Task structs will borrow from this string slice.
let master_task_list = String::from("Buy groceries. Finish Rust chapter. Call Mom. Plan weekend trip.");
let descriptions: Vec<&str> = master_task_list.split(". ").collect();
let mut tasks: Vec<Task> = Vec::new();
// Create tasks, borrowing from the 'descriptions' slices
for desc in descriptions {
tasks.push(Task::new(desc));
}
println!("\nInitial Tasks:");
for task in &tasks { // Iterate over immutable references to tasks
task.display();
}
// Let's mark one task as completed
if let Some(task_to_complete) = tasks.get_mut(1) { // Get a mutable reference to the second task
task_to_complete.mark_completed();
}
println!("\nTasks after completion:");
for task in &tasks { // Iterate again, seeing the update
task.display();
}
// What if we try to create a task with a temporary string slice?
// This would NOT compile because the temporary string would be dropped too soon.
// Try to uncomment the block below and see the compiler error!
// let temp_task;
// {
// let temp_desc_owner = String::from("This is a temporary description");
// temp_task = Task::new(temp_desc_owner.as_str());
// } // temp_desc_owner is dropped here, making temp_task.description a dangling reference!
// println!("Temporary task: ");
// temp_task.display();
// ๐ Uncommenting the above block will result in a compile-time error:
// `borrowed value does not live long enough` or `temporary value dropped while still in use`
// This is the borrow checker and lifetimes in action, preventing a bug!
println!("\n--- Task Manager Finished ---");
}
Explanation of main:
let master_task_list = String::from(...): ThisStringowns the actual text for our task descriptions. It’s crucial that thisStringlives for the entire duration that anyTaskstruct refers to its parts.let descriptions: Vec<&str> = master_task_list.split(". ").collect();: We split the master string into individual&strslices. These slices borrow frommaster_task_list.tasks.push(Task::new(desc)): EachTaskstruct we create borrows itsdescriptionfrom one of these&strslices. Becausemaster_task_listlives until the end ofmain, allTaskinstances will have valid references.for task in &tasks: When we iterate, we take immutable references to theTaskstructs, so we don’t take ownership of them.tasks.get_mut(1): We useget_mutto safely obtain anOption<&mut Task>. If the task exists, we get a mutable reference to it, allowing us to callmark_completed. This is where the “one mutable reference” rule applies. Whiletask_to_completeis active, no other mutable references to that specific task (or any immutable references to that task) could be created.
Run this code with cargo run. You should see the task manager initialize, display tasks, mark one as complete, and then display the updated list.
This example clearly demonstrates how lifetimes ensure that our Task structs always refer to valid data, preventing dangling references and making our program safe and robust.
Mini-Challenge: Enhancing the Task Manager
Your mission, should you choose to accept it, is to add a small feature to our task_manager.
Challenge:
Modify the Task struct and add a method that allows you to set a task’s priority. The priority should be a number (e.g., u8), and the method should take a mutable reference to the Task so it can update the priority field. Display the priority when printing the task.
Hints:
- You’ll need to add a
priority: u8,field to theTaskstruct. - Update the
Task::newconstructor to initializepriority(maybe to a default value like 1, or provide it as an argument). - Create a new method, say
set_priority(&mut self, new_priority: u8). - Remember to update the
displaymethod to show the priority. - In
main, after creating tasks, try setting the priority for one of them and observe the output.
What to observe/learn:
- How adding new fields affects a struct that already uses lifetimes.
- Reinforce the use of mutable references (
&mut self) for modifying struct data. - Practice calling methods on borrowed structs.
Common Pitfalls & Troubleshooting
“Borrowed value does not live long enough” or “Temporary value dropped while still in use”: This is the classic lifetime error. It means you’re trying to use a reference after the data it points to has gone out of scope and been dropped.
- Solution: Re-evaluate the scopes of your variables. Can you extend the lifetime of the owned data? Or perhaps you need to
clone()the data if you truly need an independent copy that outlives the original? Sometimes, returning an owned value (Stringinstead of&str) is the correct approach.
- Solution: Re-evaluate the scopes of your variables. Can you extend the lifetime of the owned data? Or perhaps you need to
“Cannot borrow
Xas mutable because it is also borrowed as immutable” (or vice-versa): You’re violating the “One Writer or Many Readers” rule.- Solution: Look at where your references are active. Can you shorten the lifetime of the immutable references? Or perhaps restructure your code so that the mutable operation happens when no other references are active?
clippyoften helps identify unnecessary long borrows. (For advanced scenarios, Rust offers “interior mutability” types likeRefCellorMutex, but those come with their own set of rules and are for later chapters.)
- Solution: Look at where your references are active. Can you shorten the lifetime of the immutable references? Or perhaps restructure your code so that the mutable operation happens when no other references are active?
Overusing
clone(): Whileclone()can solve borrow checker errors, it comes at the cost of performance (copying data).- Solution: Always try to solve borrow checker issues with references first. Only resort to
clone()when you genuinely need an independent, owned copy of the data. Often, passing references is sufficient and more efficient.
- Solution: Always try to solve borrow checker issues with references first. Only resort to
Forgetting
mut: Trying to modify data through an immutable reference, or trying to create a mutable reference to an immutable variable.- Solution: Ensure the original variable is declared
mut(let mut my_var = ...), and that you’re using a mutable reference (&mut my_var) when modification is intended.
- Solution: Ensure the original variable is declared
Remember, the borrow checker is your friend! It helps you write safe, high-performance code by catching potential issues at compile time rather than letting them become runtime bugs. Embrace its feedback, read the error messages carefully, and you’ll master it in no time.
Summary
Phew! You’ve just tackled some of the most fundamental and powerful concepts in Rust. Let’s recap what we’ve learned:
- Borrowing (
&and&mut): Allows you to use data without taking ownership, preventing unnecessary copying and enabling flexible data sharing. - Immutable References (
&T): Allow reading data. You can have many active at once. - Mutable References (
&mut T): Allow reading and writing data. You can only have one active at a time. - The “One Writer or Many Readers” Rule: At any given time, you can have either one mutable reference OR any number of immutable references, but never both simultaneously. This prevents data races.
- Lifetimes (
'a): Are scopes that the Rust compiler uses to ensure references never outlive the data they point to, preventing dangling references. - Lifetime Elision: Rust often infers lifetimes automatically, making code cleaner.
- Explicit Lifetimes: Needed when the compiler can’t infer the relationship between multiple references (e.g., in functions returning a reference derived from inputs, or in structs holding references).
'staticLifetime: For data that lives for the entire duration of the program.
Mastering borrowing and lifetimes is a significant milestone in your Rust journey. It’s the key to writing safe, concurrent, and efficient Rust applications.
What’s Next?
Now that we have a solid grip on Rust’s memory safety model, we’re ready to explore how to organize our data more effectively. In Chapter 6, we’ll dive into Structs, Enums, and Pattern Matching, learning how to create custom data types and elegantly work with them to build more expressive and robust applications. Get ready to design your own types!
References
- The Rust Programming Language (Official Book) - Chapter 4: Understanding Ownership: https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html
- The Rust Programming Language (Official Book) - Chapter 10: Generic Types, Traits, and Lifetimes: https://doc.rust-lang.org/book/ch10-03-lifetimes.html
- Rust Standard Library Documentation - References: https://doc.rust-lang.org/std/index.html#references
- Rust by Example - References and Borrowing: https://doc.rust-lang.org/rust-by-example/scope/borrow.html
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.