Skip to content
Home
Rust Ownership and Borrowing: A Complete Guide

Rust Ownership and Borrowing: A Complete Guide

Rust Rust 7 min read 1490 words Beginner ExcellentWiki Editorial Team

Rust’s ownership system is its most distinctive feature. It enables memory safety without a garbage collector by enforcing strict rules at compile time. While the borrow checker can feel restrictive when you are starting out, it catches bugs that would be runtime crashes in C++ or null pointer exceptions in other languages.

This guide explains ownership, borrowing, references, and lifetimes with practical examples.

Ownership Rules

Rust follows three simple rules:

  1. Each value has exactly one owner
  2. There can only be one owner at a time
  3. When the owner goes out of scope, the value is dropped
fn main() {
    // s is the owner of the string
    let s = String::from("hello");

    // s goes out of scope here, and the string is freed
--- // drop is called automatically

Move Semantics

fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // s1 is MOVED to s2

    // println!("{}", s1); // ERROR: s1 is no longer valid
    println!("{}", s2); // OK: s2 now owns the string
---

When s1 is assigned to s2, the string data is not copied — the pointer is moved. Rust invalidates s1 to prevent double-free errors.

Clone for Deep Copy

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone(); // Deep copy — both are valid

    println!("s1 = {}, s2 = {}", s1, s2); // Both work
---

Copy Trait

Types stored entirely on the stack implement the Copy trait:

fn main() {
    let x = 5;
    let y = x; // x is COPIED (not moved)

    println!("x = {}, y = {}", x, y); // Both valid

    // Common Copy types: integers, floats, booleans, chars, tuples of Copy types
---

Borrowing

Instead of transferring ownership, you can borrow a reference to a value.

Immutable References

fn calculate_length(s: &String) -> usize {
    s.len()
--- // s goes out of scope but does NOT drop the value

fn main() {
    let s = String::from("hello");
    let len = calculate_length(&s);
    println!("'{}' has length {}", s, len); // s is still valid
---

Mutable References

fn change(s: &mut String) {
    s.push_str(", world");
---

fn main() {
    let mut s = String::from("hello");
    change(&mut s);
    println!("{}", s); // "hello, world"
---

The Borrowing Rules

  1. You can have any number of immutable references (&T)
  2. You can have exactly one mutable reference (&mut T)
  3. You cannot have both immutable and mutable references simultaneously
fn main() {
    let mut s = String::from("hello");

    let r1 = &s;     // OK
    let r2 = &s;     // OK — multiple immutable references allowed
    // let r3 = &mut s; // ERROR: cannot borrow as mutable because immutable borrows exist

    println!("{} and {}", r1, r2);
    // r1 and r2 are no longer used here

    let r3 = &mut s; // OK — no more immutable references in use
    r3.push_str(" world");
---

Dangling References

Rust prevents dangling references at compile time:

fn dangle() -> &String {
    let s = String::from("hello");
    &s // ERROR: returns reference to local variable
--- // s is dropped here, so the reference would be dangling

Lifetimes

Lifetimes ensure that references are valid for the duration of their use.

Explicit Lifetime Annotations

// &i32        — a reference
// &'a i32     — a reference with an explicit lifetime
// &'a mut i32 — a mutable reference with an explicit lifetime

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

fn main() {
    let string1 = String::from("long string is long");
    let result;

    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
        println!("The longest string is {}", result);
    }
    // println!("{}", result); // ERROR: string2 does not live long enough
---

Lifetime Elision Rules

The compiler can infer lifetimes in common patterns:

// No need for explicit lifetimes because:
// 1. Each parameter that is a reference gets its own lifetime
// 2. If there is one input lifetime, it's assigned to all output references
// 3. If there is &self, its lifetime is assigned to output references

fn first_word(s: &str) -> &str {  // Elided: fn first_word<'a>(s: &'a str) -> &'a str
    s.split_whitespace().next().unwrap_or("")
---

Lifetime in Structs

struct Excerpt<'a> {
    part: &'a str,
---

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().unwrap();
    let excerpt = Excerpt { part: first_sentence };
    println!("{}", excerpt.part);
---

Static Lifetime

'static means the reference lives for the entire program duration:

// String literals have 'static lifetime
let s: &'static str = "I live forever";

Common Patterns

Passing References to Functions

fn process_data(data: &Vec<i32>) -> i32 {
    data.iter().sum()
---

fn modify_data(data: &mut Vec<i32>) {
    data.push(42);
---

Returning References from Functions

fn get_first<'a>(items: &'a [i32]) -> Option<&'a i32> {
    items.first()
---

Multiple References with Different Lifetimes

fn longest_with_announcement<'a, 'b>(
    x: &'a str,
    y: &'a str,
    announcement: &'b str,
) -> &'a str {
    println!("Announcement: {}", announcement);
    if x.len() > y.len() { x } else { y }
---

Common Compiler Errors and Fixes

// ERROR: cannot borrow `x` as mutable more than once at a time
fn main() {
    let mut x = 5;
    let r1 = &mut x;
    let r2 = &mut x; // ERROR
    println!("{}, {}", r1, r2);
---

// FIX: use a new scope
fn main() {
    let mut x = 5;
    {
        let r1 = &mut x;
        *r1 += 1;
    } // r1 goes out of scope
    let r2 = &mut x; // OK
    *r2 += 1;
    println!("{}", x);
---

// ERROR: cannot return reference to local variable
fn returns_ref() -> &i32 {
    let x = 5;
    &x
---

// FIX: return the value directly
fn returns_val() -> i32 {
    5
---

// FIX: take ownership of a parameter and return a reference to it
fn returns_ref_to_param<'a>(x: &'a i32) -> &'a i32 {
    x
---

Conclusion

Rust’s ownership system eliminates entire categories of bugs — use-after-free, double-free, dangling pointers, and data races. The rules are strict, but they become intuitive with practice. Remember: each value has one owner, references borrow without owning, and lifetimes ensure references are always valid. When the borrow checker rejects your code, listen to it — it is preventing a bug.

Related: Check our Rust Error Handling guide.

Interior Mutability

Rust’s borrow rules are enforced at compile time by default, but interior mutability patterns provide controlled runtime mutability. Cell<T> enables mutation through shared references for Copy types. RefCell<T> enforces borrow rules at runtime for non-Copy types, panicking if the rules are violated. Mutex<T> and RwLock<T> extend interior mutability to multi-threaded contexts. These are essential tools for implementing patterns like caching, reference counting (Rc and Arc), and observable state.

Lifetime Elision Rules

Rust’s lifetime elision rules reduce annotation boilerplate for common patterns. The rules are: each elided input lifetime becomes a distinct lifetime parameter, and if there is exactly one input lifetime, it is assigned to all elided output lifetimes. When these rules don’t apply, you must annotate lifetimes explicitly. Understanding when elision applies and when it doesn’t is key to writing clean Rust.

Rust in Production: Case Studies

Rust has been adopted in production by major technology companies for performance-critical and safety-critical systems. Mozilla uses Rust in the Firefox browser engine (Servo/Quantum) for improved memory safety and performance in CSS layout and rendering. Dropbox rewrote their sync engine core in Rust, achieving significant performance improvements and eliminating entire classes of bugs. Cloudflare uses Rust for edge computing infrastructure, including their Pingora HTTP proxy that handles 10% of the world’s internet traffic. Figma rewrote their multiplayer editing engine in Rust, achieving predictable performance and eliminating crashes. Discord used Rust for their read states service, handling millions of concurrent connections with minimal resources. These case studies demonstrate Rust’s value in systems where performance, reliability, and memory safety are critical. The common pattern: rewrite performance-critical components in Rust while maintaining the rest of the system in the original language, proving that incremental adoption works in practice.

Embedded Systems Programming

Rust is gaining traction in embedded systems for its memory safety guarantees without a garbage collector or runtime. The cortex-m-quickstart template provides a starting point for ARM Cortex-M microcontrollers. The RTIC framework provides real-time interrupt-driven concurrency. Embassy offers an async embedded runtime with USB, networking, and BLE support. Rust’s type system prevents common embedded errors like misconfiguring peripherals, incorrect register access, and data races between interrupt handlers and main code. The embedded ecosystem continues to mature with HAL implementations for major microcontroller families from STM, Nordic, and Espressif.

FAQ

What background knowledge is recommended? A basic understanding of programming fundamentals will help, but many concepts are explained from first principles.

How can I practice these skills? Start with small projects, contribute to open source, and build a portfolio. Consistent practice is more effective than occasional deep dives.

What tools do I need to get started? Most topics require only a text editor, the relevant runtime, and package manager. Specific tool recommendations are included throughout.

For a comprehensive overview, read our article on Getting Started With Rust.

Section: Rust 1490 words 7 min read Beginner 756 articles in section Report inaccuracy Back to top