[2026] Rust Ownership Debugging Case Study | Fixing the borrow checker says no

[2026] Rust Ownership Debugging Case Study | Fixing the borrow checker says no

이 글의 핵심

Solve real Rust ownership, borrowing, and lifetime errors beginners hit: reading borrow checker messages, RefCell, Rc, Arc, and multithreaded patterns—without fighting the compiler.

Introduction

“It works in C++—why not in Rust?” is something you hear a lot when learning Rust. This article uses real ownership errors to show how to understand the borrow checker and fix them.

What you will learn

  • How to read borrow checker error messages
  • How ownership, borrowing, and lifetimes play out in practice
  • Patterns with RefCell, Rc, Arc, and more
  • How to work with Rust’s memory safety guarantees

Table of contents

  1. Case 1: “cannot move out of borrowed content”
  2. Case 2: “cannot borrow as mutable more than once”
  3. Case 3: “lifetime may not live long enough”
  4. Case 4: “cannot return reference to local variable”
  5. Case 5: Shared state across threads
  6. Pattern cheat sheet: what to use when
  7. Conclusion

1. Case 1: “cannot move out of borrowed content”

Problem code

아래 코드는 rust를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

struct User {
    name: String,
    email: String,
}
fn process_users(users: &Vec<User>) {
    for user in users {
        let name = user.name; // ❌ cannot move out of `user.name`
        println!("Processing: {}", name);
    }
}

Error message

아래 코드는 code를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

error[E0507]: cannot move out of `user.name` which is behind a shared reference
  --> src/main.rs:8:20
   |
8  |         let name = user.name;
   |                    ^^^^^^^^^ move occurs because `user.name` has type `String`, which does not implement the `Copy` trait

Why is this an error?

  • users is an immutable borrow (&Vec<User>)
  • user.name is a String (owned type)
  • let name = user.name tries to move ownership out
  • You cannot move out of borrowed data!

Fixes

다음은 rust를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// Option 1: use references only
fn process_users(users: &Vec<User>) {
    for user in users {
        let name = &user.name; // ✅ borrow
        println!("Processing: {}", name);
    }
}
// Option 2: clone
fn process_users(users: &Vec<User>) {
    for user in users {
        let name = user.name.clone(); // ✅ copy the string
        println!("Processing: {}", name);
    }
}
// Option 3: take ownership
fn process_users(users: Vec<User>) { // remove &
    for user in users {
        let name = user.name; // ✅ move is allowed
        println!("Processing: {}", name);
    }
}

2. Case 2: “cannot borrow as mutable more than once”

Problem code

다음은 rust를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

struct ChatRoom {
    users: Vec<User>,
    messages: Vec<String>,
}
impl ChatRoom {
    fn broadcast(&mut self, sender: &User) {
        let msg = format!("{}: Hello", sender.name);
        
        // ❌ cannot borrow `self` as mutable more than once
        for user in &mut self.users {
            self.messages.push(msg.clone()); // 💥 second mutable borrow!
        }
    }
}

Error message

아래 코드는 code를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

error[E0499]: cannot borrow `self.messages` as mutable more than once at a time
  --> src/main.rs:12:13
   |
10 |         for user in &mut self.users {
   |                     --------------- first mutable borrow occurs here
11 |             self.messages.push(msg.clone());
   |             ^^^^^^^^^^^^^ second mutable borrow occurs here

Why is this an error?

  • &mut self.users is the first mutable borrow
  • self.messages.push() tries a second mutable borrow
  • Rust allows only one mutable reference at a time

Fixes

다음은 rust를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// Option 1: split borrows across fields
impl ChatRoom {
    fn broadcast(&mut self, sender: &User) {
        let msg = format!("{}: Hello", sender.name);
        
        let users = &mut self.users;
        let messages = &mut self.messages;
        
        for user in users {
            messages.push(msg.clone()); // ✅ different fields
        }
    }
}
// Option 2: push the message before the loop
impl ChatRoom {
    fn broadcast(&mut self, sender: &User) {
        let msg = format!("{}: Hello", sender.name);
        self.messages.push(msg.clone()); // add first
        
        for user in &mut self.users {
            // no longer borrowing messages here
            user.notify();
        }
    }
}
// Option 3: use indices
impl ChatRoom {
    fn broadcast(&mut self, sender: &User) {
        let msg = format!("{}: Hello", sender.name);
        
        for i in 0..self.users.len() {
            self.messages.push(msg.clone()); // ✅ indexing is not a borrow conflict
        }
    }
}

3. Case 3: “lifetime may not live long enough”

Problem code

다음은 rust를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

struct UserCache {
    users: Vec<User>,
}
impl UserCache {
    fn find(&self, name: &str) -> Option<&User> {
        self.users.iter().find(|u| u.name == name)
    }
    
    fn get_or_create(&mut self, name: &str) -> &User {
        // ❌ lifetime error
        if let Some(user) = self.find(name) {
            return user; // 💥 borrow active but &mut self needed below
        }
        
        self.users.push(User { name: name.to_string(), email: String::new() });
        self.users.last().unwrap()
    }
}

Error message

아래 코드는 code를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

error[E0502]: cannot borrow `self.users` as mutable because it is also borrowed as immutable
  --> src/main.rs:15:9
   |
12 |         if let Some(user) = self.find(name) {
   |                             ---- immutable borrow occurs here
13 |             return user;
   |                    ---- returning this value requires that `*self` is borrowed for `'1`
...
15 |         self.users.push(...);
   |         ^^^^^^^^^^^ mutable borrow occurs here

Fixes

다음은 rust를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// Option 1: use an index
impl UserCache {
    fn get_or_create(&mut self, name: &str) -> &User {
        if let Some(idx) = self.users.iter().position(|u| u.name == name) {
            return &self.users[idx]; // ✅ fresh borrow
        }
        
        self.users.push(User { name: name.to_string(), email: String::new() });
        self.users.last().unwrap()
    }
}
// Option 2: Entry API (HashMap)
use std::collections::HashMap;
struct UserCache {
    users: HashMap<String, User>,
}
impl UserCache {
    fn get_or_create(&mut self, name: &str) -> &User {
        self.users.entry(name.to_string())
            .or_insert_with(|| User { name: name.to_string(), email: String::new() })
    }
}

4. Case 4: “cannot return reference to local variable”

Problem code

아래 코드는 rust를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

fn get_default_user() -> &User {
    let user = User {
        name: "Guest".to_string(),
        email: "guest@example.com".to_string(),
    };
    &user // ❌ `user` is dropped at the end of the function
}

Error message

아래 코드는 code를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

error[E0515]: cannot return reference to local variable `user`
  --> src/main.rs:8:5
   |
8  |     &user
   |     ^^^^^ returns a reference to data owned by the current function

Fixes

다음은 rust를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// Option 1: return ownership
fn get_default_user() -> User {
    User {
        name: "Guest".to_string(),
        email: "guest@example.com".to_string(),
    }
}
// Option 2: 'static lifetime (illustrative; String cannot be const here)
fn get_default_user() -> &'static User {
    static DEFAULT: User = User {
        name: String::new(), // ❌ not valid in const—example only
    };
    &DEFAULT
}
// Option 2b: lazy_static
use lazy_static::lazy_static;
lazy_static! {
    static ref DEFAULT_USER: User = User {
        name: "Guest".to_string(),
        email: "guest@example.com".to_string(),
    };
}
fn get_default_user() -> &'static User {
    &DEFAULT_USER
}
// Option 3: heap allocation with Box
fn get_default_user() -> Box<User> {
    Box::new(User {
        name: "Guest".to_string(),
        email: "guest@example.com".to_string(),
    })
}

5. Case 5: Shared state across threads

Problem code

다음은 rust를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

use std::thread;
struct Counter {
    count: i32,
}
fn main() {
    let counter = Counter { count: 0 };
    
    let handle = thread::spawn(|| {
        counter.count += 1; // ❌ closure may outlive the current function
    });
    
    handle.join().unwrap();
}

Error message

아래 코드는 code를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

error[E0373]: closure may outlive the current function, but it borrows `counter`, which is owned by the current function
  --> src/main.rs:9:31
   |
9  |     let handle = thread::spawn(|| {
   |                                ^^ may outlive borrowed value `counter`
10 |         counter.count += 1;
   |         ------- `counter` is borrowed here

Fixes

다음은 rust를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// Option 1: Arc + Mutex (thread-safe sharing)
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
    let counter = Arc::new(Mutex::new(0));
    let counter_clone = Arc::clone(&counter);
    
    let handle = thread::spawn(move || {
        let mut count = counter_clone.lock().unwrap();
        *count += 1;
    });
    
    handle.join().unwrap();
    println!("Count: {}", *counter.lock().unwrap());
}
// Option 2: channels (message passing)
use std::sync::mpsc;
fn main() {
    let (tx, rx) = mpsc::channel();
    
    thread::spawn(move || {
        tx.send(1).unwrap();
    });
    
    let count = rx.recv().unwrap();
    println!("Count: {}", count);
}
// Option 3: atomic type (AtomicI32)
use std::sync::atomic::{AtomicI32, Ordering};
fn main() {
    let counter = Arc::new(AtomicI32::new(0));
    let counter_clone = Arc::clone(&counter);
    
    let handle = thread::spawn(move || {
        counter_clone.fetch_add(1, Ordering::SeqCst);
    });
    
    handle.join().unwrap();
    println!("Count: {}", counter.load(Ordering::SeqCst));
}

6. Pattern cheat sheet: what to use when

Ownership vs borrowing

SituationApproachExample
Read-onlyImmutable reference &Tfn print(user: &User)
Mutation neededMutable reference &mut Tfn update(user: &mut User)
Need ownershipValue Tfn consume(user: User)
Copy typesCopy traitfn calc(x: i32)

Interior mutability

TypeUse caseThread-safe
Cell<T>Interior mutability for Copy typesNo
RefCell<T>Runtime borrow checksNo
Mutex<T>Shared mutation across threadsYes
RwLock<T>Read-heavy workloadsYes

Shared ownership

TypeUse caseThread-safe
Rc<T>Single-threaded sharingNo
Arc<T>Multi-threaded sharingYes
Weak<T>Break reference cyclesDepends

Common combinations

다음은 rust를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// Single-threaded: Rc + RefCell
use std::rc::Rc;
use std::cell::RefCell;
let shared = Rc::new(RefCell::new(vec![1, 2, 3]));
let clone = Rc::clone(&shared);
shared.borrow_mut().push(4);
println!("{:?}", clone.borrow()); // [1, 2, 3, 4]
// Multi-threaded: Arc + Mutex
use std::sync::{Arc, Mutex};
let shared = Arc::new(Mutex::new(vec![1, 2, 3]));
let clone = Arc::clone(&shared);
thread::spawn(move || {
    clone.lock().unwrap().push(4);
});

7. Hands-on example: event system

C++-style (does not compile)

다음은 rust를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

struct EventManager {
    listeners: Vec<Box<dyn Fn(&Event)>>,
}
impl EventManager {
    fn subscribe(&mut self, callback: impl Fn(&Event) + 'static) {
        self.listeners.push(Box::new(callback));
    }
    
    fn publish(&self, event: &Event) {
        for listener in &self.listeners {
            listener(event); // ✅ this part is fine
        }
    }
}
// Problem: what if a subscriber wants to mutate the EventManager?
fn main() {
    let mut mgr = EventManager { listeners: vec![] };
    
    mgr.subscribe(|event| {
        mgr.publish(event); // ❌ cannot borrow `mgr` as mutable
    });
}

Rust-style (Rc + RefCell)

다음은 rust를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

use std::rc::Rc;
use std::cell::RefCell;
struct EventManager {
    listeners: Vec<Box<dyn Fn(&Event)>>,
}
fn main() {
    let mgr = Rc::new(RefCell::new(EventManager { listeners: vec![] }));
    let mgr_clone = Rc::clone(&mgr);
    
    mgr.borrow_mut().subscribe(Box::new(move |event| {
        // use mgr_clone inside the closure
        mgr_clone.borrow().publish(event);
    }));
}

8. Debugging tips

How to read the error message

아래 코드는 code를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

error[E0502]: cannot borrow `x` as mutable because it is also borrowed as immutable
  --> src/main.rs:10:5
   |
8  |     let r = &x;
   |             -- immutable borrow occurs here
9  |     
10 |     x.push(1);
   |     ^^^^^^^^^ mutable borrow occurs here
11 |     println!("{}", r);
   |                    - immutable borrow later used here

Reading order:

  1. Error code: E0502 (conflicting borrows)
  2. Where it breaks: line 10
  3. Cause: immutable borrow on line 8
  4. Where it is still used: through line 11

rustc —explain

아래 코드는 bash를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

$ rustc --explain E0502
This error indicates that you are trying to borrow a value as mutable when it
is already borrowed as immutable.
...

Clippy

아래 코드는 bash를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

$ cargo clippy
warning: this `RefCell` Ref is held across an 'await' point
  --> src/main.rs:10:9
   |
10 |     let data = cell.borrow();
   |         ^^^^
   |
   = help: consider using a `Mutex` instead

Conclusion

The borrow checker feels restrictive at first, but it guarantees memory safety at compile time. With these cases you have:

  1. Learned to read error messages carefully
  2. Understood how ownership, borrowing, and lifetimes interact
  3. Picked up RefCell, Rc, Arc, and related patterns
  4. Started thinking in a way that differs from C++ Takeaway: Do not fight the borrow checker—learn Rust’s rules and lean into them.

FAQ

Q1. Is Rust harder than C++? The early learning curve is steep, but once you are comfortable you can prevent entire classes of memory bugs. Q2. Should I avoid overusing RefCell? RefCell checks borrows at runtime and can panic. Prefer compile-time borrowing when possible. Q3. Does Arc<Mutex<T>> hurt performance? Yes, there is overhead. Use it only where needed; if reads dominate, consider RwLock.


Practical checklist

Borrow checker troubleshooting

  • Read the full error message
  • Trace borrow scopes (where they start and end)
  • Decide whether you need ownership or only a reference
  • Separate mutable vs immutable borrows
  • Check lifetime relationships
  • Pick the right pattern (Rc, RefCell, Arc, Mutex)
  • Run Clippy for extra hints

Multi-threaded safety

  • Verify Send / Sync
  • Consider Arc + Mutex
  • Watch for deadlocks
  • Consider channels
  • Use atomics when appropriate

Keywords

Rust, Ownership, Borrow Checker, Borrowing, Lifetime, RefCell, Rc, Arc, Mutex, Case Study, Debugging, Error Resolution

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3