Skip to Content
Course content

20: Practice Exercise: Fixing Common Borrow Checker Errors

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I remember the first time I hit a wall with the borrow checker. I felt like I was arguing with a compiler that was just being pedantic for the sake of it. But once you realize the borrow checker isn't trying to stop you from writing code—it's trying to stop you from crashing your program in production—the perspective shifts. Let's look at a piece of code that looks perfectly reasonable in C# or Java, but will make Rust throw a fit.

struct Project {
    name: String,
    tasks: Vec,
}

fn add_task_to_first_project(projects: &mut Vec, task: String) {
    // We grab a reference to the first project to update it
    let first = &projects[0]; 

    // Now we want to add a new empty project to the list for some reason
    projects.push(Project { 
        name: "New Project".to_string(), 
        tasks: vec![] 
    });

    // Now we try to use that first reference again
    first.tasks.push(task); 
}

If you try to compile this, the compiler will scream at you. It'll tell you that you can't borrow projects as mutable because it's already borrowed as immutable. You might think, "Why? I'm just adding a new project to the end of the list; it shouldn't affect the first one."

The Memory Shift Trap

Here is what's actually happening under the hood. When you call projects.push(...), the Vec might run out of capacity. When that happens, Rust allocates a larger chunk of memory elsewhere and moves all the existing elements to that new location.

If Rust allowed the code above, the first variable would be pointing to the old memory location—memory that has just been freed. This is a "use-after-free" bug, and it's exactly the kind of memory corruption that leads to security vulnerabilities. The borrow checker isn't being annoying; it's saving you from a segfault that would have been a nightmare to debug in a larger system.

Solving the Conflict with Indexing

To fix this, we need to make sure we aren't holding a reference to the interior of the Vec while we are mutating the Vec itself. The simplest way to do this is to avoid storing the reference in a variable entirely. Instead, we perform our mutation on the Vec first, and then access the element we need by its index right when we need it.

fn add_task_to_first_project(projects: &mut Vec, task: String) {
    // Move the mutation of the container to the top
    projects.push(Project { 
        name: "New Project".to_string(), 
        tasks: vec![] 
    });

    // Access the element by index ONLY when we are ready to use it.
    // This borrow starts and ends right here.
    projects[0].tasks.push(task); 
}

By doing this, the immutable borrow of projects[0] doesn't exist while projects.push is happening. The borrow checker is happy because there is no overlap. I've found that whenever I see this error, the first thing I ask myself is: "Am I holding onto a reference longer than I actually need to?" Usually, the answer is yes.




📋 Practical Task

Exercise: Fixing the Inventory Manager

You are building a simple inventory system. You have a Warehouse struct that contains a list of Items. You've written a function that is supposed to find a specific item by its ID and "archive" it by moving it into a separate archived_items list. However, the code currently fails to compile because of a borrow checker error.

Your Goal: Modify the archive_item function so that it compiles and runs correctly. You must ensure that you are not holding a reference to an item in the inventory list while simultaneously trying to mutate that list (by removing the item).

struct Item {
    id: u32,
    name: String,
}

struct Warehouse {
    inventory: Vec,
    archived_items: Vec,
}

impl Warehouse {
    fn archive_item(&mut self, item_id: u32) {
        // BUGGY CODE:
        // This currently fails because 'target' borrows self.inventory,
        // and then self.inventory.remove() tries to borrow it mutably.
        let target = self.inventory.iter().find(|i| i.id == item_id);

        if let Some(item) = target {
            // We need to find the index of this item to remove it
            let index = self.inventory.iter().position(|i| i.id == item_id).unwrap();
            
            // This line causes the borrow checker error
            let removed_item = self.inventory.remove(index);
            self.archived_items.push(removed_item);
        }
    }
}

fn main() {
    let mut wh = Warehouse {
        inventory: vec![
            Item { id: 1, name: "Wrench".to_string() },
            Item { id: 2, name: "Hammer".to_string() },
        ],
        archived_items: vec![],
    };

    wh.archive_item(1);
    println!("Archived items count: {}", wh.archived_items.len());
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.