Skip to content
ExponaBLOG
RUST PROGRAMMING · DAY 005 / 100

Rust ownership explained: a hands-on guide for engineers

The core idea behind Rust's safety.

Expona Admin7 min read

If you've worked in languages like C++ or Python, Rust's ownership system might feel alien at first—but it's the foundation of why Rust code is both memory-safe and blazingly fast. We'll walk through the core ownership rules with concrete examples so you can write Rust confidently in production.

The core ownership model

Rust ownership explained: a hands-on guide for engineers starts with understanding that every value in Rust has exactly one owner at any given time. This isn't a garbage collector deciding when to free memory; it's a compile-time contract. The owner is responsible for cleaning up the value. When ownership changes hands, the previous owner can no longer access that value.

This single-owner constraint removes entire categories of bugs. No use-after-free. No double-free. No data races. The compiler verifies all of this before your code ships.

Think of it like a car title. Only one person owns the car. When you sell it, the title transfers. You can't drive a car you no longer own—the compiler won't let you.

One owner—Each value has exactly one owner

In Rust, when you bind a value to a variable, that variable becomes the owner. Let's look at a real scenario from a web backend where you're processing user data:

rust
fn main() {
    let user_name = String::from("alice@example.com");
    // user_name is the owner of the String
    println!("User: {}", user_name); // OK
    // user_name still owns the value here
}
// user_name goes out of scope, the String is freed automatically

The variable user_name owns the heap-allocated String. As long as it's in scope, you can use it. When the function returns or the block ends, that ownership is released and memory is reclaimed.

Now let's add a function call to this pattern. When you pass a value to a function, you're also passing ownership:

rust
fn log_user(name: String) {
    println!("Logging: {}", name);
    // name is owner here
}
// name goes out of scope, String is freed

fn main() {
    let user = String::from("bob@example.com");
    log_user(user);
    // After this call, user no longer owns the String
    // println!("{}", user); // ERROR: value used after move
}

Once you pass user to log_user, you've transferred ownership. The function parameter becomes the new owner. When the function ends, that memory is freed. You can't use user afterward—the compiler catches this at compile time.

Moves—Assigning a String moves ownership

When you assign one variable to another, ownership moves rather than copies. This is the behavior that catches many developers off guard coming from languages without explicit ownership:

rust
fn main() {
    let a = String::from("hi");
    let b = a;          // a is moved to b
    // println!("{a}"); // error: use of moved value
    println!("{b}");    // OK: b is now the owner
}

After let b = a;, the ownership of that String has moved from a to b. Variable a is no longer valid. If you tried to use it, the compiler would reject your code with a clear error message: "value used after move."

This is powerful in production systems because it forces you to think about resource lifetime. Let's see a more realistic example from an e-commerce order pipeline:

rust
struct Order {
    id: u64,
    items: Vec<String>,
}

fn process_order(order: Order) {
    // order is now owned by this function
    println!("Processing order {}", order.id);
    // Order is freed here when the function ends
}

fn main() {
    let my_order = Order {
        id: 12345,
        items: vec!["widget".to_string(), "gadget".to_string()],
    };
    
    process_order(my_order); // ownership moves
    
    // my_order is no longer accessible
    // println!("{:?}", my_order); // ERROR
}

Once my_order is passed to process_order, the function takes ownership. When process_order completes, the Order and all its heap-allocated data are cleaned up automatically. No memory leaks, no dangling pointers.

Drop—Values are freed when the owner goes out of scope

Rust calls the cleanup method Drop. For most standard types, the compiler automatically implements this. When a value goes out of scope, drop is called, and memory is reclaimed.

rust
fn main() {
    {
        let buffer = String::from("temporary data");
        println!("{}", buffer); // OK
    } // buffer goes out of scope, drop is called, memory freed
    
    // println!("{}", buffer); // ERROR: not in scope
}

You can manually implement Drop for custom types to clean up resources like database connections or file handles:

rust
struct DbConnection {
    url: String,
}

impl Drop for DbConnection {
    fn drop(&mut self) {
        println!("Closing connection to {}", self.url);
        // Actual cleanup happens here
    }
}

fn main() {
    let _conn = DbConnection {
        url: "postgres://localhost".to_string(),
    };
    println!("Connection created");
} // drop is called here: "Closing connection to..."

This pattern is critical in production systems. You never need to manually free memory or close handles. The compiler ensures cleanup happens in the right order, preventing resource leaks that plague C++ codebases.

Copy types—Integers and other simple types are copied, not moved

Not everything moves. Small, stack-allocated types like integers, booleans, and floats implement the Copy trait. When you assign them, they're copied, not moved:

rust
fn main() {
    let x = 42;
    let y = x;          // x is copied, not moved
    println!("x: {}, y: {}", x, y); // Both OK
}

You can use x after assigning it to y because Copy types are duplicated rather than transferred. This makes sense for a simple integer—copying 8 bytes is cheap.

The rule: if a type is Copy, assignment copies it. Otherwise, assignment moves it. Standard Copy types include:

  • Integers: i32, u64, etc.
  • Floats: f32, f64
  • Booleans: bool
  • Characters: char
  • Tuples of Copy types: (i32, i32)

Heap-allocated types like String, Vec<T>, and HashMap<K, V> are not Copy—they move:

rust
fn main() {
    let nums = vec![1, 2, 3]; // Vec is not Copy
    let nums_copy = nums;      // nums is moved
    // println!("{:?}", nums); // ERROR: value used after move
    println!("{:?}", nums_copy); // OK
}

In a trading system or any high-frequency application, you'd want to avoid moving large collections unnecessarily. That's where references come in—but that's the topic for the next day in this series.

Common mistakes

Mistake 1: Trying to use a value after moving it

rust
let order = String::from("order_123");
process(order);
println!("{}", order); // ERROR: borrow of moved value

Fix: Either use a reference (we'll cover this) or restructure to use the value before passing it away.

Mistake 2: Forgetting that function parameters take ownership

rust
fn log_data(data: Vec<u64>) {
    println!("{:?}", data);
}

fn main() {
    let my_data = vec![1, 2, 3];
    log_data(my_data);
    println!("{:?}", my_data); // ERROR: moved value
}

Fix: Pass a reference &Vec<u64> instead if you need to keep using my_data.

Mistake 3: Assuming Copy semantics for all simple types

Even though tuples can be Copy, if they contain non-Copy types, they're not Copy:

rust
let data = (String::from("hello"), 42);
let copy = data;
// println!("{:?}", data); // ERROR: String is not Copy

Key takeaways

  • Every value has exactly one owner; ownership can be transferred through assignment or function calls.
  • When a value is moved, the previous owner can no longer use it—the compiler enforces this at compile time.
  • Values are automatically freed (via the Drop trait) when their owner goes out of scope, eliminating manual memory management.
  • Copy types (integers, booleans, floats) are duplicated on assignment; non-Copy types (strings, vectors, collections) are moved.
  • Understanding ownership prevents use-after-free bugs, double-frees, and data races before code reaches production.
  • When you need to share a value without moving it, use references (covered in the next day's guide).
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles