Working with Ownership

Having trouble with ownership in Rust? This guide provides practical solutions to common ownership challenges including avoiding unnecessary clones, working with multiple owners, and solving borrow checker errors.

Problem: Value Moved When You Still Need It

Scenario

You want to use a value in multiple places but get “value moved” errors.

let data = String::from("hello");
process(data);           // data moved here
println!("{}", data);    // Error: value used after move

Solution 1: Borrow Instead of Move

Pass references when you don’t need to transfer ownership.

fn process(data: &String) {
    println!("Processing: {}", data);
}

fn main() {
    let data = String::from("hello");
    process(&data);          // Borrow, don't move
    println!("{}", data);    // Still valid
}

How it works: References (&T) allow reading without taking ownership. Original owner retains control.

Solution 2: Clone When Necessary

Clone the value when you need independent copies.

fn process(data: String) {
    // Takes ownership
    println!("Processing: {}", data);
}

fn main() {
    let data = String::from("hello");
    process(data.clone());   // Clone for process
    println!("{}", data);    // Original still valid
}

When to use: When function needs to own the data or modify it independently.

Trade-off: Cloning has performance cost (heap allocation for String).

Solution 3: Return Ownership

Have the function return ownership back.

fn process(data: String) -> String {
    println!("Processing: {}", data);
    data  // Return ownership
}

fn main() {
    let data = String::from("hello");
    let data = process(data);  // Move in, get back
    println!("{}", data);
}

Use case: When processing doesn’t need to keep the value.


Problem: Cannot Borrow as Mutable Multiple Times

Scenario

You need multiple mutable references but Rust prevents it.

let mut data = vec![1, 2, 3];
let r1 = &mut data;
let r2 = &mut data;  // Error: cannot borrow as mutable more than once
r1.push(4);
r2.push(5);

Solution 1: Sequential Borrows

Use references sequentially, not simultaneously.

let mut data = vec![1, 2, 3];

{
    let r1 = &mut data;
    r1.push(4);
}  // r1 goes out of scope

let r2 = &mut data;
r2.push(5);

How it works: Rust allows mutable borrows when they don’t overlap.

Solution 2: Split Mutable Borrows

Split collections to allow multiple mutable references.

let mut data = vec![1, 2, 3, 4, 5, 6];
let (first_half, second_half) = data.split_at_mut(3);

first_half[0] = 10;
second_half[0] = 20;

How it works: split_at_mut returns two non-overlapping slices that can be mutated independently.

Solution 3: Interior Mutability (RefCell)

Use RefCell<T> for runtime borrow checking.

use std::cell::RefCell;

let data = RefCell::new(vec![1, 2, 3]);

data.borrow_mut().push(4);
data.borrow_mut().push(5);

Warning: Runtime panics if you violate borrowing rules. Use with caution.

Use case: When you need flexible borrowing patterns that can’t be verified at compile time.


Problem: Lifetime Errors with References

Scenario

Function returns a reference but compiler can’t determine lifetime.

fn first_word(s: &str) -> &str {
    s.split_whitespace().next().unwrap()
}

// Error: missing lifetime specifier

Solution 1: Lifetime Elision

Often lifetimes are inferred automatically.

fn first_word(s: &str) -> &str {
    s.split_whitespace().next().unwrap()
}

fn main() {
    let sentence = String::from("hello world");
    let word = first_word(&sentence);
    println!("{}", word);
}

How it works: Compiler infers that output lifetime matches input lifetime.

Solution 2: Explicit Lifetimes

Make lifetime relationships explicit when necessary.

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

fn main() {
    let s1 = String::from("long string");
    let s2 = String::from("short");
    let result = longest(&s1, &s2);
    println!("{}", result);
}

How it works: 'a indicates returned reference lives as long as the shorter of x or y.

Solution 3: Return Owned Data

Avoid lifetime issues by returning owned values.

fn first_word_owned(s: &str) -> String {
    s.split_whitespace()
        .next()
        .unwrap()
        .to_string()
}

Trade-off: Allocates new String, but no lifetime constraints.


Problem: Cannot Move Out of Borrowed Content

Scenario

You try to move a value out of a borrowed reference.

let vec = vec![String::from("a"), String::from("b")];
let borrowed = &vec;
let owned = borrowed[0];  // Error: cannot move out of index

Solution 1: Clone the Value

Create an owned copy.

let vec = vec![String::from("a"), String::from("b")];
let borrowed = &vec;
let owned = borrowed[0].clone();
println!("{}", owned);

Solution 2: Take Ownership of Container

If you own the container, you can move out.

let mut vec = vec![String::from("a"), String::from("b")];
let owned = vec.remove(0);  // Removes and returns ownership
println!("{}", owned);

Note: This modifies the vector.

Solution 3: Use Option::take()

For Option<T>, use take() to move value out.

let mut opt = Some(String::from("value"));
if let Some(value) = opt.take() {
    println!("{}", value);
}
// opt is now None

Problem: Sharing Data Across Threads

Scenario

You need to share data between multiple threads.

use std::thread;

let data = vec![1, 2, 3];

// Error: cannot share Vec across threads
thread::spawn(|| {
    println!("{:?}", data);
});

Solution 1: Arc for Shared Ownership

Use Arc<T> for thread-safe reference counting.

use std::sync::Arc;
use std::thread;

let data = Arc::new(vec![1, 2, 3]);
let mut handles = vec![];

for i in 0..3 {
    let data_clone = Arc::clone(&data);
    let handle = thread::spawn(move || {
        println!("Thread {}: {:?}", i, data_clone);
    });
    handles.push(handle);
}

for handle in handles {
    handle.join().unwrap();
}

How it works: Arc allows multiple owners. Reference count tracks owners, deallocates when count reaches zero.

Solution 2: Arc<Mutex> for Mutation

Combine Arc and Mutex for shared mutable state.

use std::sync::{Arc, Mutex};
use std::thread;

let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];

for _ in 0..10 {
    let counter_clone = Arc::clone(&counter);
    let handle = thread::spawn(move || {
        let mut num = counter_clone.lock().unwrap();
        *num += 1;
    });
    handles.push(handle);
}

for handle in handles {
    handle.join().unwrap();
}

println!("Result: {}", *counter.lock().unwrap());

How it works: Mutex ensures only one thread accesses data at a time. Arc allows sharing across threads.


Problem: Struct with References Has Lifetime Issues

Scenario

Struct contains references but lifetimes are complex.

struct Parser<'a> {
    input: &'a str,
    position: usize,
}

impl<'a> Parser<'a> {
    fn new(input: &'a str) -> Self {
        Parser { input, position: 0 }
    }
}

Solution 1: Explicitly Annotate Lifetimes

Clearly specify lifetime relationships.

struct Parser<'a> {
    input: &'a str,
    position: usize,
}

impl<'a> Parser<'a> {
    fn new(input: &'a str) -> Parser<'a> {
        Parser { input, position: 0 }
    }

    fn current(&self) -> &'a str {
        &self.input[self.position..]
    }
}

fn main() {
    let text = String::from("hello world");
    let parser = Parser::new(&text);
    println!("{}", parser.current());
}

Solution 2: Own the Data Instead

Store owned data to avoid lifetime annotations.

struct Parser {
    input: String,
    position: usize,
}

impl Parser {
    fn new(input: String) -> Self {
        Parser { input, position: 0 }
    }

    fn current(&self) -> &str {
        &self.input[self.position..]
    }
}

fn main() {
    let parser = Parser::new(String::from("hello world"));
    println!("{}", parser.current());
}

Trade-off: Requires cloning input if you don’t own it, but eliminates lifetime complexity.


Problem: Temporary Value Dropped While Borrowed

Scenario

Reference to temporary value that goes out of scope.

let x = &String::from("hello").as_str();  // Error: temporary value dropped
println!("{}", x);

Solution 1: Store Temporary in Variable

Give the temporary a longer lifetime.

let temp = String::from("hello");
let x = temp.as_str();
println!("{}", x);

Solution 2: Use String Literals

For constant strings, use literals.

let x = "hello";  // &'static str
println!("{}", x);

Common Pitfalls

Pitfall 1: Excessive Cloning

Problem: Cloning everything to avoid ownership issues.

// Inefficient
fn process(data: Vec<i32>) {
    // ...
}

let vec = vec![1, 2, 3];
process(vec.clone());
process(vec.clone());
process(vec.clone());

Solution: Borrow when possible.

fn process(data: &[i32]) {
    // ...
}

let vec = vec![1, 2, 3];
process(&vec);
process(&vec);
process(&vec);

Pitfall 2: Fighting the Borrow Checker

Problem: Complex borrowing patterns that fight Rust’s rules.

Solution: Restructure code to work with ownership system. Consider:

  • Breaking function into smaller parts
  • Using different data structures (e.g., indices instead of references)
  • Cloning strategically when necessary
  • Using interior mutability (RefCell, Mutex) as last resort

Pitfall 3: Unnecessary Lifetime Annotations

Problem: Adding lifetime annotations when they’re not needed.

// Unnecessary
fn first<'a>(x: &'a str) -> &'a str { x }

// Sufficient (lifetime elision works)
fn first(x: &str) -> &str { x }

Related Patterns

  • Builder Pattern: Construct complex objects without ownership issues
  • Interior Mutability: Cell<T>, RefCell<T> for controlled mutability
  • Smart Pointers: Box<T>, Rc<T>, Arc<T> for flexible ownership
  • RAII: Resource Acquisition Is Initialization for automatic cleanup

Related Resources


Master Rust ownership to write safe, efficient code!

Last updated