Rust Fundamentals

Ownership and Borrowing


Learning Objectives

  • You understand Rust’s ownership rules and why they exist.
  • You can explain what happens when values are moved.
  • You know how to borrow values using references.
  • You understand the difference between mutable and immutable references.
  • You can work with String and &str types.

Warming up

Here is a set of tiny programming tasks to get you familiar with syntax that relates to ownership and borrowing. You can solve these exercises directly in the embedded code editor.

Loading Exercise...

Ownership and Rust

In many programming languages, you either manually manage memory (which is error-prone) or rely on a garbage collector (which adds runtime overhead). Rust takes a different approach: it uses a system of ownership with a set of rules that the compiler checks at compile time.

The ownership system ensures that:

  • Memory is freed automatically when it’s no longer needed
  • You can’t accidentally use memory after it’s been freed
  • You can’t have data races in concurrent code

While this might sound complex, ownership is handled in large part by the compiler, and for a programmer it suffices to only know a couple of basic concepts and rules instead of the whole theory behind it.

The Three Rules of Ownership

The three rules of ownership are the foundation of Rust’s memory management system. They govern how values are owned, moved, and borrowed in Rust programs. They are as follows:

  1. Each value in Rust has a variable that’s called its owner.
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value is dropped (freed).

Loading Exercise...

Ownership in Action

Let’s start with a simple example using strings. We’ll create a String that stores the name of a coffee drink:

Here, drink is the owner of the String value “Espresso”. When drink goes out of scope at the end of the main function, the String is automatically freed.

Moving Values

Now, let’s see what happens when we assign one variable to another:

When we write let drink2 = drink1;, the ownership of the String is moved from drink1 to drink2. After this point, drink1 is no longer valid. If you uncomment the last line, you’ll get a compiler error:

error[E0382]: borrow of moved value: `drink1`

This is Rust preventing you from using a value after it has been moved. The data itself hasn’t been copied - just the ownership has transferred.

Fig 1. — When a value is moved, the original variable becomes invalid.

Passing Values to Functions

The same thing happens when you pass a value to a function:

When we call print_drink(order), ownership of the String moves into the function. After the function returns, the String is dropped. The variable order in main is no longer valid.

This might seem restrictive, but it prevents a whole class of bugs! You can’t accidentally use a value after it’s been freed.

Loading Exercise...

Some Types are Automatically Copied Instead of Moved

Although we just saw that ownership moves by default, not all types behave this way. See, for example, this code with integers:

Why can we use cups1 after assigning it to cups2? The reason is that integers implement the Copy trait (we’ll look into traits later in this part). Types that implement Copy are automatically copied instead of moved. This is safe for simple types like integers, booleans, and floats because they have a known, fixed size and live on the stack.

Note: Types that implement Copy include:

  • All integer types (i32, u64, etc.)
  • All floating-point types (f32, f64)
  • bool
  • char
  • Tuples, if they only contain types that implement Copy

String does not implement Copy because it manages data on the heap and copying it would be expensive.

Loading Exercise...

References and Borrowing

If moving values makes them unusable, how do we write useful programs? The answer is borrowing. Instead of giving ownership of a value to a function, we can lend it using a reference.

Immutable References

A reference allows you to refer to a value without taking ownership of it. References are created with the & operator:

Now print_drink takes a reference to a String (&String) instead of a String. When we call it, we pass &order - a reference to order. The function can read the value, but it doesn’t own it. After the function returns, we can still use order in main.

This is called borrowing: the function borrows the value but gives it back when it’s done.

Fig 2. — Borrowing allows temporary access without taking ownership.

Multiple Immutable References

You can have as many immutable references to a value as you want:

This is safe because none of these references can modify the value - they can only read it.

Mutable References

What if you need to modify a value? For that, you need a mutable reference, created with &mut:

Note: Both the variable (let mut order) and the reference (&mut order) must be declared as mutable.

Loading Exercise...

Loading Exercise...

Dereferencing to Read and Modify Values

When you have a reference to a value, you sometimes need to use the dereference operator (*) to access or modify the underlying value.

The * operator “follows” the reference to get to the actual value. When you write *count_ref += 3, you’re saying “get the value that count_ref points to and add 3 to it.”

Loading Exercise...

You might wonder why we didn’t need to use * in the earlier add_sugar example. The reason is that Rust automatically dereferences references when calling methods and we were calling a method (push_str) on the reference. For direct operations like assignment or arithmetic, however, you need to explicitly use *.

Here are both approaches side by side:

Note: You can also read through a reference without dereferencing when printing or comparing, because Rust’s formatting and comparison traits automatically dereference for you.

You can also use * to read values through immutable references, though it’s often not necessary:


Loading Exercise...

The Borrowing Rules

Here’s the key restriction: you can have either one mutable reference OR any number of immutable references, but not both at the same time.

This code will not compile:

fn main() {
    let mut order = String::from("Espresso");

    let ref1 = ℴ      // Immutable borrow
    let ref2 = ℴ      // Another immutable borrow - OK
    let ref3 = &mut order;  // Mutable borrow - ERROR!

    println!("{}, {}, {}", ref1, ref2, ref3);
}

The compiler will complain:

error[E0502]: cannot borrow `order` as mutable because it is also borrowed as immutable

Why this restriction? It prevents data races at compile time. If you could read a value while someone else is modifying it, you might see inconsistent data.

Fig 3. — The borrowing rules prevent data races at compile time.

However, this code will work:

This works because the immutable references (ref1 and ref2) are no longer in use when we create the mutable reference. Rust is smart enough to see that the scopes don’t overlap.

Loading Exercise...

Note: The scope of a reference starts where it’s introduced and continues through its last use, not necessarily to the end of the block. This is called Non-Lexical Lifetimes (NLL); see also the original RFC 2094.

Loading Exercise...

The String Type

In this chapter, we’ve been using String in our examples, but in the last chapter, we used string slices (&str). What’s the difference?

String vs &str

  • String is an owned, growable string type. It manages its own memory on the heap. You can create, modify, and own String values.
  • &str is a string slice - a reference to a string stored somewhere else. You can get a &str from a String, but not the other way around. You cannot modify a &str because it’s just a view into existing data.

When to Use Each

Here’s a general rule:

  • Use &str for function parameters when you just need to read the string
  • Use String when you need to own or modify the string

Notice that display_menu_item takes &str - this is flexible because:

  • You can pass string literals directly: "Espresso"
  • You can pass String references: &drink

This is why &str is the recommended type for function parameters when you only need to read the string.

Loading Exercise...

String Operations

Here are some common String operations: ,

Note: The .to_string() method converts a &str to a String. You’ll use this when you need an owned string from a string literal.

Loading Exercise...

Example: Coffee Shop Inventory

Let’s build a more complete example that demonstrates ownership and borrowing. We’ll create a simple inventory system for our coffee shop.

Let’s examine what’s happening with ownership and borrowing:

  1. check_stock borrows item immutably with &str. It only needs to read the name, not modify it. It also takes quantity by value (a copy) since i32 implements Copy.

  2. add_stock borrows item immutably (&str) and quantity mutably (&mut i32). It needs to modify the quantity, so it requires a mutable reference. The * operator dereferences the mutable reference to access and modify the value.

  3. sell_item is similar to add_stock - it needs to modify the quantity, so it takes a mutable reference.

  4. In main, we own the String values (coffee_beans and milk). We pass references to these strings to the functions, so they can use them without taking ownership. The stock quantities are mutable (mut), and we pass mutable references when we need to modify them.

Notice that we can still use coffee_beans and milk after passing them to functions because we only borrowed them - we didn’t move ownership.

Loading Exercise...

Common Mistakes and Solutions

Let’s look at some common mistakes students make when learning ownership:

Mistake 1: Trying to use a moved value

fn main() {
  let item = String::from("Coffee");
  let item2 = item;  // item is moved
  println!("{}", item);  // Error!
}

Solution: Use a reference instead, or clone the value:

Mistake 2: Multiple mutable references

fn main() {
  let mut count = 5;
  let r1 = &mut count;
  let r2 = &mut count;  // Error! Can't have two mutable references
  *r1 += 1;
  *r2 += 1;
}

Solution: Make sure mutable references don’t overlap:

Mistake 3: Mixing immutable and mutable references

fn main() {
    let mut text = String::from("Hello");
    let r1 = &text;
    let r2 = &mut text;  // Error! Can't borrow as mutable while immutable borrow exists
    println!("{} {}", r1, r2);
}

Solution: Make sure immutable borrows are done before mutable borrow:

Mistake 4: Forgetting to dereference mutable references

fn main() {
    let mut count = 5;
    let count_ref = &mut count;
    count_ref += 3;  // Error! Can't add to a reference
}

Solution: Use the dereference operator:


Loading Exercise...

Summary

In this chapter, we explored Rust’s ownership system:

  • Ownership rules: Each value has one owner, and the value is dropped when the owner goes out of scope
  • Moving values: Assignment and function calls transfer ownership
  • Copy trait: Simple types like integers are copied instead of moved
  • Borrowing: References let you access values without taking ownership
  • Immutable references (&T): Multiple allowed, can only read
  • Mutable references (&mut T): Only one allowed, can read and write
  • The borrowing rules: Either one mutable reference OR any number of immutable references
  • String types: String for owned strings, &str for string slices

The ownership system might feel restrictive at first, but it prevents entire classes of bugs:

  • No dangling pointers
  • No use-after-free errors
  • No data races
  • Memory is automatically managed safely