How to return ownership from a function

Return ownership in Rust by placing the variable name as the final expression in the function body without a semicolon to move the value to the caller.

The vault door is closing

You write a function to generate a report. You build a String with the report text inside the function body. You try to return it. The compiler accepts it. You call the function and assign the result to a variable. It works.

Now you try to return a reference to the report because you think references are faster. The compiler rejects you with E0515 (returned value does not live long enough). The reference points to memory that vanishes when the function ends. You have to return the owned string. The caller takes the memory. This is how Rust guarantees memory safety. The compiler forces you to decide who pays for the memory.

Ownership moves with the value

Functions are isolated scopes. Variables created inside a function live on the function's stack frame. When the function returns, the stack frame is destroyed. Any data that lives only on that stack frame is gone.

To keep data alive after the function returns, you must move it out. Moving transfers ownership. The value is transferred from the function's scope to the caller's scope. The caller becomes the new owner. The caller is responsible for cleaning up the value when it's done.

Think of a function like a secure factory floor. When you manufacture a widget inside the factory, the widget sits on the assembly line. When the shift ends, the factory locks up and clears the floor. If you want the widget, you have to take it out before the lock. Taking it out means you now own the widget. You can't just point to where it sat on the line. The line gets cleared. You have to carry the widget out yourself.

In Rust, returning a value is carrying the widget out. The caller receives the widget and holds the responsibility for it.

The minimal pattern

Returning ownership is straightforward. List the variable name as the final expression in the function body. Do not use a semicolon. The semicolon suppresses the value. No semicolon means "pass this value out".

/// Creates a new String and moves ownership to the caller.
fn gives_ownership() -> String {
    // Allocates heap memory for the string data.
    let some_string = String::from("hello");

    // Returns the String, moving ownership to the caller.
    // No semicolon. The value exits the function.
    some_string
}

/// Takes ownership of a String and immediately returns it.
fn takes_and_gives_back(some_string: String) -> String {
    // some_string is owned by this function now.
    // The caller can no longer use the original variable.
    // Returning it moves ownership back to the caller.
    some_string
}

The function signature declares the return type. The body provides the value. The last expression without a semicolon is the return value. Rust calls this an "implicit return". You can use the return keyword, but the convention is to omit it for the final expression. It reduces noise and makes the flow clearer.

Drop the semicolon to pass the value.

What happens on the stack

When gives_ownership runs, some_string is allocated on the heap. The stack frame holds a String struct, which is 24 bytes: a pointer to the heap, a length, and a capacity. The actual characters live on the heap.

When the function hits the return, Rust moves the 24 bytes onto the caller's stack. The heap data stays put. The pointer, length, and capacity are copied to the caller. The local variable some_string is now invalid inside the function, but since the function is ending, that doesn't matter. The caller receives the three words and becomes the owner.

If the caller drops the string, the heap memory is freed. The ownership chain is unbroken. The value was created, moved to the caller, and will be dropped by the caller.

The heap stays put. The pointer moves.

Copy types vs Move types

Not all types move. Types that implement the Copy trait are duplicated on the stack. i32, bool, f64, and tuples of Copy types implement Copy. Returning a Copy type copies the value. The original stays valid inside the function.

/// Returns a Copy type. The value is duplicated.
fn returns_copy() -> i32 {
    let x = 42;
    // Copies 42 to the caller.
    // x remains valid and usable inside this function.
    x
}

fn main() {
    let y = returns_copy();
    // y is 42. The original x inside the function is gone,
    // but that doesn't matter because it was a copy.
    println!("{}", y);
}

Returning a String moves. The original becomes invalid. Returning an i32 copies. The original stays valid. This distinction matters for performance and logic. Copy types are cheap to duplicate. Move types transfer responsibility.

Use Copy returns for small, stack-only values. Use move returns for heap-allocated or complex data.

Real code returns more than strings

Real functions rarely just return a single string. You usually process data and return a result. Maybe you parse a config and return the parsed struct. Or you calculate a length and return both the data and the length.

Tuples are the standard way to return multiple values. The tuple groups the values and moves them together.

/// Parses a raw input and returns the cleaned data along with its length.
fn process_input(raw: String) -> (String, usize) {
    // Trim whitespace and convert to lowercase.
    // to_lowercase allocates a new String.
    let cleaned = raw.trim().to_lowercase();

    // Calculate length after cleaning.
    let len = cleaned.len();

    // Return a tuple containing the owned String and the length.
    // Ownership of cleaned moves into the tuple.
    // The tuple moves to the caller.
    (cleaned, len)
}

If you return more than two or three values, consider a struct. Tuples are good for small groups. Structs pack meaning. A struct gives names to the fields, which makes the code self-documenting.

/// Result of processing input.
struct ProcessResult {
    /// The cleaned text.
    text: String,
    /// Length of the cleaned text.
    length: usize,
}

/// Returns a named struct for clarity.
fn process_input_struct(raw: String) -> ProcessResult {
    let text = raw.trim().to_lowercase();
    ProcessResult {
        text,
        length: text.len(),
    }
}

Tuples group values. Structs group meaning.

Common traps and compiler errors

The most common mistake is trying to return a reference to a local variable.

fn invalid_return() -> &String {
    let s = String::from("hello");
    &s // Error: E0515
}

The compiler rejects this with E0515 (returned value does not live long enough). s is dropped at the end of the function. Returning &s would give the caller a dangling pointer. Rust prevents this. You must return the owned value, not a reference to a local.

Another trap is the accidental semicolon. If you add a semicolon after the return expression, the function returns () instead of the value.

fn broken_return() -> String {
    let s = String::from("hello");
    s; // Returns (), not String.
    // E0308: mismatched types. Expected String, found ().
}

The compiler catches this with E0308 (mismatched types). The signature expects a String, but the body returns (). Check your semicolons. The semicolon is the difference between a gift and a void.

E0515 saves you from segfaults.

Performance and optimization

You might worry that returning a large struct copies everything. Rust optimizes this. Named Return Value Optimization (NRVO) allows the compiler to construct the return value directly in the caller's stack frame. There is no extra copy. Returning by value is efficient.

The compiler can also eliminate moves in many cases. If you return a local variable, the compiler often treats it as if it was created in the caller's frame. You don't need to return references to avoid copies. Return owned values. The optimizer handles the rest.

Returning by value is free. Trust the optimizer.

Convention asides

Mark functions that return values which shouldn't be ignored with #[must_use]. This is a community convention for functions that return results or resources. If the caller ignores the return value, the compiler emits a warning.

#[must_use]
fn create_connection() -> Connection {
    // ...
}

This prevents resource leaks and silent errors. Use #[must_use] for functions that return Result, Option, or handles to resources.

Choosing the right return type

Use owned returns when the function creates new data that didn't exist before the call. The caller needs to manage the lifetime.

Use owned returns when transforming data where the input is consumed. You take ownership, modify, and return the new owner.

Return a reference when the data lives longer than the function. The input owns the data, and you just point to a slice of it.

Return Result<T, E> when the operation can fail. The caller must handle the error before accessing the owned value.

Return Option<T> when the value might not exist. The caller checks for None before using the data.

Use Copy returns for small, stack-only values like integers and booleans. The value is duplicated, and the original stays valid.

Match the return type to the data's lifetime.

Where to go next