Question

Rust Borrow Checker: Immutable and Mutable Borrows of a Graph

rustreference

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 &T references or one &mut T reference, 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.

Take Quiz

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:

  1. graph.get_node_by_value("source") borrows graph as &Graph.
  2. It searches graph.nodes and returns Option<&Node>.
  3. unwrap() extracts the &Node, which is stored in source.
  4. println! reads source.value. This is the final use of the borrowed reference.
  5. The immutable borrow can now end.
  6. graph.add_node(...) borrows graph as &mut Graph and 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 &str or &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?

Take Quiz

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

SituationAllowed?Reason
Multiple &Node references from a graphYesShared references can coexist.
One &mut Graph referenceYesIt has exclusive access.
&Node alive while requesting &mut GraphNoA mutable borrow must be exclusive.
Read a node, finish using it, then mutate graphYesThe immutable borrow has ended.
Clone node data, then mutate graphYesThe clone is owned and no longer borrows the graph.

&T versus &mut T

ReferenceMeaningHow many may exist?
&TRead-only shared accessMany
&mut TRead and write exclusive accessOne, with no active &T references

Reference versus owned value

ChoiceExampleCan the graph be mutated afterward?
Borrowed valuelet node: &Node = ...Only after the reference is no longer used
Owned clonelet name: String = node.value.clone()Yes, immediately after cloning
Identifierlet id: NodeId = ...Yes, but the ID needs a validity strategy
Take Quiz

Cheat Sheet

  • &T is a shared, read-only borrow.
  • &mut T is an exclusive, mutable borrow.
  • Valid rule: many &T or one &mut T.
  • A return type such as Option<&Node> means the caller receives a reference into self.
  • That reference keeps the owner immutably borrowed until its last use.
  • A method taking &mut self cannot 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.

Take Quiz

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

  1. Define a Node type containing a String name.
  2. Define a Graph type that stores nodes in a vector.
  3. Add an add_node method that ignores duplicate names.
  4. Add a get_node_by_value method that returns Option<&Node>.
  5. Look up and print one node before adding another node.
  6. Print the final graph for verification.
Take Quiz

Keep learning

Related questions

View more