Skip to content
All essays
CraftMarch 13, 202411 min

Rust Borrowing and References

Master borrowing and references for flexible, safe code in Rust

Ü
Ümit Uz
Mobile & Full Stack Developer

Borrowing is Rust's mechanism for temporarily accessing data without taking ownership. It's a powerful feature that enables both flexibility and safety, letting you work with data in multiple ways while maintaining memory safety guarantees.

Understanding Borrowing

Borrowing allows you to reference data without transferring ownership. This is essential for writing flexible Rust code that doesn't constantly move values around.

The Basics

References come in two flavors: immutable and mutable.

rust
let s = String::from("hello");
let len = calculate_length(&s);
println!("Length of '{}' is {}", s, len);

The &s syntax creates a reference to s, allowing you to use it without taking ownership.

Immutable References

Immutable references let you read data without modifying it.

Multiple Immutable References

You can have multiple immutable references simultaneously:

rust
let s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);

This is safe because multiple readers can't cause data races.

Reference Scope

References are valid only within a certain scope:

rust
let s = String::from("hello");
{
    let r1 = &s;
    let r2 = &s;
    println!("{} and {}", r1, r2);
} // r1 and r2 go out of scope
let r3 = &s; // OK

Mutable References

Mutable references allow you to modify data through a reference.

Exclusive Mutable Access

You can have only one mutable reference at a time:

rust
let mut s = String::from("hello");
let r1 = &mut s;
r1.push_str(", world");
// let r2 = &mut s; // Error!
println!("{}", r1);

This prevents data races by enforcing exclusive write access.

Combining Mutable and Immutable

You can't have mutable and immutable references simultaneously:

rust
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
// let r3 = &mut s; // Error!
println!("{} and {}", r1, r2);

But this works:

rust
let mut s = String::from("hello");
let r1 = &s;
println!("{}", r1);
let r2 = &mut s; // OK, r1 is no longer used
r2.push_str(", world");

Reference Lifetime

References have lifetimes that determine how long they're valid.

Implicit Lifetimes

Most of the time, Rust infers lifetimes automatically:

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

The compiler ensures the returned reference is valid.

Explicit Lifetime Annotations

Sometimes you need to be explicit:

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

The 'a lifetime parameter ensures the output lives as long as the inputs.

Lifetime Rules

Rust has three lifetime rules that the compiler enforces:

  1. 1Each parameter gets its own lifetime
  2. 2If there's exactly one input lifetime, it's assigned to all output lifetimes
  3. 3If there are multiple input lifetimes but one is &self or &mut self, that lifetime is assigned to all output lifetimes

Structs with Lifetimes

Structs can hold references with lifetime annotations:

rust
struct ImportantExcerpt<'a> {
    part: &'a str,
}

let novel = String::from("Call me Ishmael...");
let first_sentence = novel.split('.').next().unwrap();
let excerpt = ImportantExcerpt {
    part: first_sentence,
};

Dangling References

Rust prevents dangling references at compile-time:

rust
fn dangle() -> &String { // Error!
    let s = String::from("hello");
    &s
}

The error: "returns a reference to data owned by the current function."

The Solution

Return the owned value instead:

rust
fn no_dangle() -> String {
    let s = String::from("hello");
    s
}

Or pass in a reference:

rust
fn no_dangle(s: &String) -> &str {
    &s[..]
}

Borrowing in Practice

Let's explore practical borrowing patterns.

String Slices

String slices are common borrowing patterns:

rust
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];

Slices are references to portions of strings.

Array Slices

Works with arrays too:

rust
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3]; // [2, 3]

Function Parameters

Functions often borrow parameters:

rust
fn print_coordinates(grid: &Vec<Vec<bool>>) {
    for row in grid {
        for cell in row {
            print!("{}", if *cell { "■" } else { "□" });
        }
        println!();
    }
}

Borrowing and Iterators

Iterators make heavy use of borrowing:

rust
let v = vec![1, 2, 3];
let iter = v.iter(); // Borrows immutably
for val in iter {
    println!("{}", val);
}

IntoIterator

Consumes the collection:

rust
let v = vec![1, 2, 3];
for val in v {
    println!("{}", val);
}
// v is no longer valid

IterMut

Mutable iteration:

rust
let mut v = vec![1, 2, 3];
for val in v.iter_mut() {
    *val *= 2;
}

Smart Pointers and Borrowing

Smart pointers provide specialized borrowing behavior:

Box<T>

Single ownership, can be borrowed:

rust
let b = Box::new(5);
let r = &b;
println!("{}", r);

Rc<T>

Multiple ownership, can be borrowed:

rust
use std::rc::Rc;

let a = Rc::new(5);
let b = Rc::clone(&a);
let r = &*a;
println!("{}", r);

RefCell<T>

Interior mutability with runtime borrow checking:

rust
use std::cell::RefCell;

let data = RefCell::new(vec![1, 2, 3]);
let borrowed = data.borrow(); // Immutable borrow
let mut_borrowed = data.borrow_mut(); // Panic!

Borrowing Rules in Methods

Methods borrow self in various ways:

rust
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 { // Borrows immutably
        self.width * self.height
    }

    fn set_width(&mut self, width: u32) { // Borrows mutably
        self.width = width;
    }
}

Common Borrowing Patterns

The AsRef Trait

Convert to references:

rust
fn print<T: AsRef<str>>(s: T) {
    println!("{}", s.as_ref());
}

print("hello");
print(String::from("hello"));

The Borrow Trait

Convert between borrowed forms:

rust
use std::borrow::Borrow;

fn eq<T: Borrow<str>>(a: T, b: &str) -> bool {
    a.borrow() == b
}

Cow (Clone on Write)

Borrow or clone as needed:

rust
use std::borrow::Cow;

fn modify_string(s: Cow<str>) -> Cow<str> {
    if s.contains("foo") {
        let mut owned = s.into_owned();
        owned.push_str("bar");
        Cow::Owned(owned)
    } else {
        s
    }
}

Borrowing and Closures

Closures capture variables by various borrowing strategies:

Immutable Borrow

rust
let list = vec![1, 2, 3];
let iter = list.iter();

Mutable Borrow

rust
let mut list = vec![1, 2, 3];
list.push(4);
let mut borrows_mutably = || list.push(5);
borrows_mutably();

Take Ownership

rust
let list = vec![1, 2, 3];
let takes_ownership = move || {
    println!("{:?}", list);
};
takes_ownership();

Borrow Checker Errors

Understanding common errors helps you write better Rust.

Use After Move

rust
let s = String::from("hello");
let s2 = s;
println!("{}", s); // Error!

Fix: Use s.clone() or borrow s2.

Mutable While Borrowed

rust
let mut s = String::from("hello");
let r = &s;
s.push_str(" world"); // Error!

Fix: Drop the reference before mutation.

Lifetime Mismatch

rust
fn returns_str() -> &'static str {
    let s = String::from("hello");
    &s // Error!
}

Fix: Return owned String or use 'static lifetime.

Advanced Borrowing

Multiple Lifetimes

Functions can have multiple lifetime parameters:

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

Lifetime Subtyping

Longer lifetimes can substitute for shorter ones:

rust
fn choose_first<'a: 'b, 'b>(first: &'a str, second: &'b str) -> &'b str {
    first
}

Trait Objects with Lifetimes

rust
trait Foo {}

fn use_foo<'a>(foo: &'a dyn Foo) {
    // Use foo
}

Borrowing and Concurrency

Borrowing enables safe concurrent programming:

rust
use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    let handle = thread::spawn(|| {
        println!("Here's a vector: {:?}", v);
    });

    handle.join().unwrap();
}

Move Closures

For concurrency, use move:

rust
let v = vec![1, 2, 3];

let handle = thread::spawn(move || {
    println!("Here's a vector: {:?}", v);
});

Debugging Borrowing Issues

The compiler provides helpful messages.

Reading Errors

"borrow of moved value" - use after move "cannot borrow as mutable" - existing immutable borrow "does not live long enough" - lifetime issue

Compiler Suggestions

Rust often suggests fixes:

rust
// Consider borrowing or cloning the data
let s2 = s.clone();

Best Practices

  1. 1Prefer borrowing over cloning
  2. 2Keep borrows as short as possible
  3. 3Use references for function parameters
  4. 4Understand lifetime elision rules
  5. 5Use Cow for optional cloning
  6. 6Design APIs to minimize borrowing constraints

Performance Considerations

Borrowing has zero runtime cost:

rust
let s = String::from("hello");
let r = &s; // Just a pointer

References are just pointers under the hood, with compile-time guarantees.

Conclusion

Borrowing is Rust's secret sauce for flexible, safe code. It lets you work with data without ownership transfers, enabling powerful patterns while maintaining memory safety.

Master borrowing, and you'll write Rust code that's both ergonomic and efficient. The borrow checker is your friend, guiding you toward correct code that avoids entire classes of bugs.

Practice with borrowing until it feels natural. It's the key to unlocking Rust's full potential for building reliable, performant software.

Enjoyed this essay? Share it.

Related essays

Next essay
React Context API Patterns