Question
Why does Rust report error E0502, "cannot borrow as mutable because it is also borrowed as immutable", in this graph code?
#[derive(Debug)]
struct Node {
value: String,
}
#[derive(Debug)]
pub struct Graph {
nodes: Vec<Box<Node>>,
}
fn mk_node(value: String) -> Node {
Node { value }
}
pub fn mk_graph() -> Graph {
Graph { nodes: vec![] }
}
impl Graph {
fn add_node(&mut self, value: String) {
if self.nodes.iter().position(|node| node.value == value).is_none() {
let node = Box::new(mk_node(value));
self.nodes.push(node);
}
}
fn get_node_by_value(&self, value: &str) -> Option<&Node> {
match self.nodes.iter().position(|node| node.value == value) {
None => None,
Some(index) => self.nodes.get(index).map(|node| &**node),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn some_test() {
let mut graph = mk_graph();
graph.add_node("source".to_string());
graph.add_node("destination".to_string());
let source = graph.get_node_by_value("source").unwrap();
let destination = graph.get_node_by_value("destination").unwrap();
graph.add_node("destination".to_string());
}
}
Why is it invalid to call graph.add_node(...) after obtaining &Node references from graph, and why does the Queue example allow calls to both push and pop?
Short Answer
Rust references are tied to the value they borrow from. get_node_by_value returns an immutable reference into graph, so Rust must prevent mutable access to graph for as long as that reference could still be used. This rule prevents references from observing invalid or unexpectedly changed data.
You will learn Rust's shared-versus-exclusive borrowing rule, how non-lexical lifetimes can shorten a borrow, and how to structure code when you need to read from and later mutate a collection.
Concept
Rust has two primary kinds of references:
&T: a shared (immutable) reference. Many shared references may exist at once.&mut T: an exclusive (mutable) reference. It must be the only active reference to that data.
The core rule is:
You may have many
&Treferences or one&mut Treference, but not both at the same time.
get_node_by_value takes &self and returns Option<&Node>. The returned &Node points to a Node owned by graph, so source and destination keep graph immutably borrowed.
let source: &Node = graph.get_node_by_value("source").unwrap();
By contrast, add_node needs &mut self because it may change self.nodes:
fn add_node(&mut self, value: String)
Rust cannot grant that mutable borrow while a reference into the graph is still live. A mutable operation could remove, replace, or otherwise alter the node that an existing reference points to.
Even though add_node currently does not add a duplicate node, Rust checks the method signature and its allowed behavior, not just the outcome of one particular call. Any method receiving &mut Graph is allowed to modify the graph.
Box<Node> places each node allocation on the heap, which can make a node's address stable when the Vec reallocates. However, that does not change the borrowing rule: a mutable Graph method could still remove or replace a Box<Node>, leaving an existing &Node invalid. Rust therefore does not allow the overlapping borrows.
Mental Model
Think of graph as a library archive.
- A shared borrow,
&Graph, is like visitors reading documents in the archive. Many visitors can read at once. - A mutable borrow,
&mut Graph, is like a librarian reorganizing the archive. While reorganizing, nobody may hold a document from the archive.
A Node reference such as source is a visitor holding a particular document. Rust will not let the librarian obtain an exclusive reorganization permit while that document reference may still be used.
The rule is conservative on purpose: it makes dangling references and data races impossible in safe Rust.
Syntax and Examples
A method returning a reference creates a borrow that is connected to the original owner.
struct Book {
title: String,
}
struct Library {
books: Vec<Book>,
}
impl Library {
fn find(&self, title: &str) -> Option<&Book> {
self.books.iter().find(|book| book.title == title)
}
fn add(&mut self, title: String) {
self.books.push(Book { title });
}
}
This does not compile if book is needed after the mutation:
let mut library = Library {
books: vec![Book {
title: "Rust".to_string(),
}],
};
let book = library.find("Rust").unwrap();
library.add("Systems Programming".to_string());
println!("{}", book.title);
book is still used in println!, so its immutable borrow lasts until that line. library.add(...) requires an exclusive mutable borrow in the middle, which conflicts.
Use the reference before mutating
let book = library.find("Rust").unwrap();
println!("Found: {}", book.title);
library.add("Systems Programming".to_string());
With modern Rust's non-lexical lifetimes, the immutable borrow ends after the last use of book, so the later mutation is allowed.
Own the needed data instead
If you need the value after mutation, copy or clone only the data you need:
let title = library.find("Rust").unwrap().title.clone();
library.add("Systems Programming".to_string());
println!("Previously found: {title}");
Here title is an owned String, not a reference into library.
Step by Step Execution
Consider this example:
let source = graph.get_node_by_value("source").unwrap();
println!("Source: {}", source.value);
graph.add_node("new-node".to_string());
Step by step:
graph.get_node_by_value("source")borrowsgraphas&Graph.- It searches
graph.nodesand returnsOption<&Node>. unwrap()extracts the&Node, which is stored insource.println!readssource.value. This is the final use of the borrowed reference.- The immutable borrow can now end.
graph.add_node(...)borrowsgraphas&mut Graphand can safely modify it.
Now compare it with this ordering:
let source = graph.get_node_by_value("source").unwrap();
graph.add_node("new-node".to_string());
println!("Source: {}", source.value);
At the add_node call, Rust knows source will be used later. Its immutable borrow is therefore still active, and the mutable borrow is rejected.
Why the original snippet may compile on current Rust
In the provided test, source and destination are never used after they are assigned. Modern Rust uses non-lexical lifetimes (NLL), which usually ends their borrows at their last use, so a current compiler may accept the final add_node call.
Older Rust compilers, or code where the references are used later, report E0502 because the immutable borrow is considered active until the end of its scope. The underlying rule remains the same.
Real World Use Cases
This rule appears whenever a program holds a reference into a collection and then wants to change that collection.
- API request data: borrow a field from a request, validate or log it, then mutate the request or response.
- Caching: look up an item by reference, compute information from it, then update the cache.
- Game entities: inspect an entity, finish using the borrowed data, then add or remove entities.
- Parsers: borrow a token from input, convert it into owned data if it must survive input-buffer changes.
- Configuration maps: retrieve a
&stror&Config, then update the map only after the borrowed value is no longer required.
The usual design question is: Do I need a reference into the collection, or do I need an owned value that can outlive a later mutation?
Real Codebase Usage
In real Rust projects, developers usually solve these situations with short borrows, owned values, or stable identifiers.
Keep borrows short
Use a borrowed node immediately, then mutate later.
if let Some(node) = graph.get_node_by_value("source") {
println!("Found {}", node.value);
}
// The `if let` borrow has ended.
graph.add_node("destination".to_string());
This is a useful scoped-borrow pattern.
Use a guard clause before mutation
Your add_node method already follows a common pattern: inspect first, then mutate only if necessary.
fn add_node(&mut self, value: String) {
if self.nodes.iter().any(|node| node.value == value) {
return;
}
self.nodes.push(Box::new(Node { value }));
}
The immutable borrow from iter().any(...) ends before push begins because the result is only a bool, not a reference into the vector.
Return an owned copy when needed
For small or cloneable data, return an owned value:
fn get_node_value(&self, value: &str) -> Option<String> {
self.nodes
.iter()
.find(|node| node.value == value)
.map(|node| node.value.clone())
}
Use IDs instead of long-lived references
Graph-like applications commonly give nodes an ID and store that ID in edges or later operations. An ID does not borrow the graph.
type NodeId = usize;
fn find_node_id(&self, value: &str) -> Option<NodeId> {
self.nodes.iter().position(|node| node.value == value)
}
Be careful: a usize index can become invalid as a logical identifier if nodes are removed or reordered. Production graph code often uses a dedicated ID type, a slot map, or another data structure designed for stable handles.
Common Mistakes
Assuming &mut self only affects one field
This is not allowed while a reference into the same struct exists:
let node = graph.get_node_by_value("source").unwrap();
graph.add_node("other".to_string());
println!("{}", node.value);
add_node receives &mut Graph, which means it has exclusive access to the entire Graph, not merely a single vector element.
Avoid it: use node before calling add_node, or clone the required data.
Expecting Box to bypass borrowing rules
struct Graph {
nodes: Vec<Box<Node>>,
}
A Box can stabilize an allocation's address, but it does not permit simultaneous &Graph and &mut Graph borrows. The graph can still remove or replace boxes.
Avoid it: treat Box as an ownership and allocation tool, not a borrow-checker escape hatch.
Keeping a reference when an owned value is needed
Broken pattern:
let value = &graph.get_node_by_value("source").unwrap().value;
graph.add_node("new".to_string());
println!("{value}");
value still points into graph.
Fix: clone the String if it must be retained:
let value = graph.get_node_by_value("source").unwrap().value.clone();
graph.add_node("new".to_string());
println!("{value}");
Calling drop as the first solution
You can explicitly end a reference's lifetime:
let source = graph.get_node_by_value("source").unwrap();
println!("{}", source.value);
drop(source);
graph.add_node("new".to_string());
This is valid, but usually unnecessary with modern Rust. Prefer arranging code so the last use naturally occurs before mutation, often with a smaller scope.
Using indices as permanently valid references
An index avoids a borrow, but it may no longer identify the same logical node after removal or reordering.
Avoid it: do not use raw vector indices as stable external IDs unless your collection's mutation rules guarantee their meaning.
Comparisons
| Situation | Allowed? | Reason |
|---|---|---|
Multiple &Node references from a graph | Yes | Shared references can coexist. |
One &mut Graph reference | Yes | It has exclusive access. |
&Node alive while requesting &mut Graph | No | A mutable borrow must be exclusive. |
| Read a node, finish using it, then mutate graph | Yes | The immutable borrow has ended. |
| Clone node data, then mutate graph | Yes | The clone is owned and no longer borrows the graph. |
&T versus &mut T
| Reference | Meaning | How many may exist? |
|---|---|---|
&T | Read-only shared access | Many |
&mut T | Read and write exclusive access | One, with no active &T references |
Reference versus owned value
| Choice | Example | Can the graph be mutated afterward? |
|---|---|---|
| Borrowed value | let node: &Node = ... | Only after the reference is no longer used |
| Owned clone | let name: String = node.value.clone() | Yes, immediately after cloning |
| Identifier | let id: NodeId = ... | Yes, but the ID needs a validity strategy |
Cheat Sheet
&Tis a shared, read-only borrow.&mut Tis an exclusive, mutable borrow.- Valid rule: many
&Tor one&mut T. - A return type such as
Option<&Node>means the caller receives a reference intoself. - That reference keeps the owner immutably borrowed until its last use.
- A method taking
&mut selfcannot run while such a borrow is active. - Use the reference before mutation, put it in a smaller scope, or clone the required data.
- Modern Rust often shortens borrows to the last use through non-lexical lifetimes.
Box<T>does not remove aliasing or borrowing restrictions.- IDs can avoid borrowing, but raw vector indices may become semantically invalid after removals or reordering.
FAQ
What does Rust error E0502 mean?
E0502 means Rust found a mutable borrow and an immutable borrow of the same value that overlap in time. The mutable borrow must be exclusive.
Why does get_node_by_value borrow the whole graph?
It returns &Node, and that node is owned by the graph. Rust tracks the returned reference as borrowing from graph so it cannot outlive the graph or become invalid through unsafe mutation.
Why can add_node search and then push into the same vector?
The search produces a bool or Option<usize>, not a reference that remains borrowed from the vector. Once the search expression ends, its immutable borrow ends, and push can take a mutable borrow.
Does the original test fail on every current Rust compiler?
Not necessarily. Since source and destination are not used after assignment, modern Rust can usually end those borrows early using non-lexical lifetimes. The error occurs if using an older compiler or if either reference is used after the mutation.
Should I call drop(source) to fix borrowing errors?
Only occasionally. Prefer using the reference before mutation or limiting its scope with a block or if let. Explicit drop can be appropriate when it clearly expresses that a resource or borrow is no longer needed.
Should get_node_by_value return a clone instead of &Node?
Return &Node when callers only need to inspect a node while the graph is unchanged. Return an owned value, such as a cloned field or a dedicated data type, when callers need it to survive graph mutation.
Does storing nodes in Box<Node> make references safe during vector growth?
It can keep a particular node allocation at a stable address during vector reallocation, but safe Rust still prohibits overlapping shared and mutable borrows of the graph. Mutation could remove or replace the boxed node.
Mini Project
Description
Build a small graph-like node registry that prevents duplicate node names. The project demonstrates a common borrow-friendly workflow: inspect a borrowed node, extract or print what is needed, and then safely mutate the collection.
Goal
Create a registry that adds unique nodes, looks them up by name, and can add another node after a lookup has finished being used.
Requirements
- Define a
Nodetype containing aStringname. - Define a
Graphtype that stores nodes in a vector. - Add an
add_nodemethod that ignores duplicate names. - Add a
get_node_by_valuemethod that returnsOption<&Node>. - Look up and print one node before adding another node.
- Print the final graph for verification.
Keep learning
Related questions
Absolute Values in Rust: Using the abs() Method
Learn how to calculate absolute values in modern Rust with abs(), handle signed integer overflow, and choose safer alternatives.
Accepting Iterators in Rust Functions with impl Iterator
Learn how to write Rust functions that accept HashMap iterators using generic bounds or impl Iterator, avoiding trait object and lifetime errors.
Access Enum Variant Values in Rust with Pattern Matching
Learn how to extract and print values stored in Rust enum variants using match and if let pattern matching, with practical examples.