Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Tracing Ownership and Drop Points in Rust

Hello! You have already used Rust’s blocks, functions, and types to describe what a program does. This module adds a question that matters whenever a program creates data: who is responsible for it, and when is it cleaned up? Ownership and borrowing will eventually let your backend code use strings and collections without a garbage collector or manual memory management.

In this lesson, you’ll trace values through a small program. The goal is to identify which values live entirely on the stack, which values own heap allocations, and what happens at each closing brace. We’ll leave the mechanics of transferring ownership to the next lesson.


Two kinds of memory in one value

Consider an integer such as let count: i32 = 3;. Its size is known in advance, so the value can be stored directly in the function’s stack frame. When the function finishes, that frame is removed. No separately allocated buffer needs to be freed.

A growable String has a different shape. Rust knows the size of the String value itself: it contains a pointer, a length, and a capacity. But the text it owns can vary in size, so its bytes occupy a separate allocation on the heap. The pointer in the fixed-size String value identifies that allocation. Length is the number of bytes currently used; capacity is the amount of space reserved, which may be larger.

The following short video provides a visual model of stack frames and heap storage.

Learning Rust: Memory, Ownership and Borrowing

Watch “Learning Rust: Memory, Ownership and Borrowing” by YouCodeThings for its desk-and-locker illustration of stack and heap memory.

Watch stack and heap. Focus on the distinction between a function’s fixed-size local data and a separately allocated string buffer. The video calls the stack-held information a “pointer”; for a Rust String, remember that the pointer is one of three fields, alongside length and capacity.

One distinction prevents a common misunderstanding: in String::from("queued"), the literal "queued" is text embedded in the program, while the resulting String owns a heap buffer containing its text. The String value and the allocation it manages are related, but they are not the same piece of memory. Also, for strings, length is measured in bytes, not necessarily characters; our examples use ASCII text, where those counts happen to agree.


Ownership gives cleanup a definite place

Rust assigns each value an owner. At a given time, a value has one owner; when that owner’s scope ends, Rust drops the value. A scope is the region of code in which a binding is available—often the block enclosed by a pair of braces.

Read the relevant parts of the Rust Book before tracing the program below.

What is Ownership? - The Rust Programming Language

Read the Rust Book’s explanation of ownership rules, variable scope, and String allocation. It connects the memory layout you have just seen to the point at which Rust cleans up an owned value.

In “Ownership Rules,” read the three rules. Then, in “Variable Scope,” begin with the definition of scope and read the scope explanation. Finally, under “Memory and Allocation,” read through automatic cleanup. Stop before “Variables and Data Interacting with Move”; moves are the next lesson’s subject. As you read, distinguish a String’s stack-held fields from the heap buffer its drop operation releases.

“Dropped” has a more specific meaning than “the variable name can no longer be used.” For a String, dropping the value runs its cleanup code and releases its owned heap allocation. For an i32, the scope still ends, but there is no heap buffer to release and no custom cleanup to run. The compiler may optimize how either value is stored; stack drawings are a useful model for reasoning about ownership, not a promise about every machine instruction.


Trace a complete program

Here is a small program with an inner block and a function call. Read each closing brace as a potential cleanup point.

fn print_batch() {
    let batch = String::from("batch");
    println!("{batch}");
} // batch is dropped here

fn main() {
    let records: i32 = 3;
    let first = String::from("queued");

    {
        let retries: i32 = 2;
        let second = String::from("pending");
        let third = String::from("review");

        println!("{records} {retries}: {second}, {third}");
    } // third is dropped, then second; retries leaves scope

    print_batch(); // batch has been dropped when this call returns
    println!("{records}: {first}");
} // first is dropped; records leaves scope

Begin at the start of main. records contains an integer value. first is a String that owns a heap buffer holding queued. Entering the inner block creates another integer, retries, and two more independently owned String values, second and third. At this point, three string buffers are live: the ones owned by first, second, and third. Printing their contents does not change who owns them.

At the inner closing brace, Rust deals with that block’s locals in reverse declaration order. It drops third, releasing its buffer holding review, and then drops second, releasing its buffer holding pending. retries also leaves scope, but has no heap allocation to clean up. Nothing about that brace ends first’s scope: first was declared in the surrounding main block, so its buffer remains live.

Next, print_batch() gets its own local String. Its buffer holding batch is released at the end of print_batch, before execution resumes after the call in main. Finally, main prints first and reaches its closing brace. Only then is first dropped and its buffer released. records leaves scope too, without string-like cleanup.

A compact trace helps separate scope from allocation:

Point in execution Live owners of string buffers Buffers released at this point
Inside the inner block first, second, third None
Just after the inner block first third’s, then second’s
During print_batch first, batch batch’s when the function ends
At the end of main first, until the closing brace first’s

For a program like this, a reliable tracing method is to mark each binding’s enclosing block, identify whether its type owns a resource, and then inspect each closing brace. A function’s locals have their own scope; calling it does not end the caller’s scope.

The image below shows a related memory layout, but after an ownership transfer. For now, use it to locate the fixed-size String fields and the single heap buffer. We will explain the transfer itself next time.

After ownership of the string “Banana” transfers from `fruit1` to `fruit2`, `fruit1` is invalid and `fruit2` is the sole usable owner. The pointer, length, and capacity are stack-held fields; the text occupies one heap buffer.

The invalid fruit1 drawn in the image is not a second owner that will free the buffer. That distinction is what prevents the same allocation from being cleaned up twice.


Where this leaves you

Ownership gives you a precise way to trace cleanup: find the owner, find its scope, and check whether it still owns the value when that scope ends. An integer’s scope ends without heap cleanup; dropping an owning String releases its buffer. Inner blocks and called functions create their own cleanup points without prematurely ending surrounding scopes.

So far, each String has stayed with its original binding. Next, you’ll see what changes when an assignment or function call copies a value or moves ownership to a new binding—and how that changes the eventual drop point.

Can't find a good explanation? Sign up and we'll make it for you

Sign up