String ownership and borrowship in Rust

Published on 2025-11-09

For this post, when I say "string", I am refering to a generic string of characters. But when I say "String", I am refering to the String data type in Rust.

I am very verbose in this post as I always am. Unfortunately or fortunately, this helps me understand the concept. One thing that I am not a fan of is "hand-waving".

Let's say we create a string "hello" in Rust as follow:

let s = String::from("hello");

When the Rust program executes the line above, it creates a stack-allocated memory for the variable s and a heap-allocated memory to hold the string's data (in this case that's the characters h,e, l, l, o or I will notate as "hello" hereinafter). The variable s, which is of String type is the owner of the heap-allocated memory. The stack-allocated variable s (of String type) is a 3-part data structure containing a pointer to the heap data, its length, and its capacity. The s variable, in turn is owned by the scope in which it is defined. In summary, the variable s owns the heap-allocated memory, and the scope in which s is defined is its owner. When the scope in which s is defined ends and it will eventually end, the stack-allocated memory of s will get freed. The Rust's ownership system ensures the heap-allocated memory owned by s also gets freed.

Let's examine what happens when we have the following function:

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

The last line is not ending with a semicolon, so effectively it is the same as

return s;  

Let's call create_string() from another function foo:

fn foo() {
    let str = create_string();
    println!("{}", str)
}

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

The last line s or return s; means the entire String value (which is 3-part data structure explained above) is returned. The caller's stack-frame (which is foo in this case) creates a stack-allocated variable str and Rust ownership system transfers the ownership from s in the callee's stack-frame to str in the caller's stack-frame. What's important is the heap data is not copied, but its owner goes from s to str. When the execution of create_string hits the closing }, s goes out of scope, but s no longer owns any memory, Rust's ownership rules does not touch the heap-allocated memory it used to own but no longer owns. The heap-allocated memory is now owned by str. Let's say later in the program, when foo ends, the scope of str ends, Rust will now deallocate the heap-allocated memory of "hello".

In the next example, we will modify the function create_string so that instead of return the String, we will return the reference to that String, notated as &String. Since the function does a different "thing" we will change its name accordingly. Let's name it create_and_return_dangling_string. The reason for the name that has the word dangling will be clear later.

fn create_and_return_dangling_string()i -> &String {
    let s = String::from("hello");
    &s
}  

At first glance, it is obvious that this is never allowed. This initial intuition is correct. The variable s is stack-allocated. Its scope ends when the function exits. A reference to a deallocated memory obviously causes a dangling reference. In other words, s does not live long enough. Rust, being a very smart compiler, will never allow such a code to compile.

We can stop here and with that knowledge, we have enough to write Rust programs.

But let's play double advocate and challenge ourselves by asking a follow-up question. For this, let's modify foo() to call create_and_return_dangling_string():

fn foo() {
    let str = create_and_return_dangling_string();
    println!("{}", str)
}

In the first line, doesn't the ownership of the heap-allocated memory get transferred from s to str as we talked earlier?

The answer is No because in Rust, when we use the operator & to assign a reference to an existing variable like this

let str = &s;    

This assignment of reference does not transfer ownership. The variable s still owns the data. What the above line does is saying that str borrows without taking the ownership of the data. So back to our example, this line

let str = create_and_return_dangling_string();

does not cause the ownership to be transfered to str, but str only borrows the heap-allocated memory ("hello") that is still owned by s. So when s goes away, its owned memory in the heap also goes away. Rust compiler of course cannot allow str to borrow some memory that eventually go away. Therefore, our little program won't compile.

There are many books, videos and resources explaining ownership and borrowship. But I find that understanding and re-explaining to myself in a way that does not involve any hand-waving, I am able to retain the knowledge more efficiently.