Guide contents

Lesson 13 of 94

Ownership & Functions

Passing a value to a function works exactly like assigning it to a variable: Copy types are copied and everything else is moved. Returning a value moves it back out to the caller. This lesson walks through the consequences, and ends with the problem that the next lesson, borrowing, solves.

  • fn save(s: String) receives ownership; the caller can no longer use s.
  • Returning a value moves ownership to the caller.
  • A value lives until its current owner's scope ends.

1. Passing a String moves it into the function

Rust✗ does not compile · E0382
fn print_banner(text: String) {
    println!("*** {text} ***");
}

fn main() {
    let title = String::from("Welcome");
    print_banner(title);
    println!("{title}");
}
Output
error[E0382]: borrow of moved value: `title`
 --> src/main.rs:8:16
  |
6 |     let title = String::from("Welcome");
  |         ----- move occurs because `title` has type `String`, which does not implement the `Copy` trait
7 |     print_banner(title);
  |                  ----- value moved here
8 |     println!("{title}");
  |                ^^^^^ value borrowed here after move
  |
note: consider changing this parameter type in function `print_banner` to borrow instead if owning the value isn't necessary
 --> src/main.rs:1:23
  |
1 | fn print_banner(text: String) {
  |    ------------       ^^^^^^ this parameter takes ownership of the value
  |    |
  |    in this function
help: consider cloning the value if the performance cost is acceptable
  |
7 |     print_banner(title.clone());
  |                       ++++++++

The parameter text becomes the owner. When print_banner returns, text goes out of scope and the String is dropped, so title would be pointing at freed memory. Rust prevents that by making title unusable after the call.

2. The function drops what it owns

Rust
fn consume(label: String) {
    println!("consuming '{label}'");
} // `label` goes out of scope: the String is freed here

fn main() {
    let a = String::from("first");
    let b = String::from("second");
    consume(a);
    consume(b);
    println!("both strings have been freed");
}
Output
consuming 'first'
consuming 'second'
both strings have been freed

3. Copy values are copied into functions

Rust
fn double(n: i32) -> i32 {
    n * 2
}

fn main() {
    let base = 21;
    let result = double(base);
    println!("{base} doubled is {result}");
}
Output
21 doubled is 42

4. Returning a value moves ownership to the caller

A function can create a value and hand it out. The value is not freed when the function ends, because it has a new owner.

Rust
fn make_greeting(name: &str) -> String {
    let greeting = format!("Hello, {name}!");
    greeting // ownership moves to the caller
}

fn main() {
    let message = make_greeting("Grace");
    println!("{message}");
}
Output
Hello, Grace!

5. Taking ownership and giving it back

A function can take ownership, change the value, and return it. This is a real and idiomatic pattern for "transform this value".

Rust
fn add_signature(mut email: String) -> String {
    email.push_str("\n--\nSent from my Rust program");
    email
}

fn main() {
    let draft = String::from("Hi team, the release is out.");
    let final_email = add_signature(draft);
    println!("{final_email}");
}
Output
Hi team, the release is out.
--
Sent from my Rust program

mut email makes the parameter mutable inside the function. The caller's value did not need to be mut, because the caller gave it away.

6. Re-using the same variable name

Shadowing makes the take-and-give-back pattern read naturally: the name stays the same while the value flows through the function.

Rust
fn add_title(mut name: String) -> String {
    name.insert_str(0, "Dr. ");
    name
}

fn main() {
    let name = String::from("Ferris");
    let name = add_title(name);
    println!("{name}");
}
Output
Dr. Ferris

7. A chain of owned transformations

Each function takes a String and returns a new or modified one. Ownership flows straight through, and nothing is copied.

Rust
fn trim_spaces(s: String) -> String {
    s.trim().to_string()
}

fn lowercase(s: String) -> String {
    s.to_lowercase()
}

fn replace_spaces(s: String) -> String {
    s.replace(' ', "-")
}

fn main() {
    let title = String::from("  Rust Ownership Explained  ");
    let slug = replace_spaces(lowercase(trim_spaces(title)));
    println!("{slug}");
}
Output
rust-ownership-explained

8. Consuming a Vec and returning a new one

Rust
fn shout_all(words: Vec<String>) -> Vec<String> {
    let mut result = Vec::new();
    for word in words {           // consumes `words`
        result.push(word.to_uppercase());
    }
    result
}

fn main() {
    let words = vec![String::from("stop"), String::from("look"), String::from("listen")];
    let loud = shout_all(words);
    println!("{:?}", loud);
}
Output
["STOP", "LOOK", "LISTEN"]

9. Modifying a Vec you were given, then returning it

Rust
fn add_defaults(mut headers: Vec<(String, String)>) -> Vec<(String, String)> {
    headers.push((String::from("Accept"), String::from("application/json")));
    headers.push((String::from("User-Agent"), String::from("rust-client/1.0")));
    headers
}

fn main() {
    let headers = vec![(String::from("Authorization"), String::from("Bearer abc123"))];
    let headers = add_defaults(headers);
    for (name, value) in &headers {
        println!("{name}: {value}");
    }
}
Output
Authorization: Bearer abc123
Accept: application/json
User-Agent: rust-client/1.0

10. Calling a consuming function in a loop

Rust✗ does not compile · E0382
fn send(message: String) {
    println!("sending: {message}");
}

fn main() {
    let message = String::from("heartbeat");
    for _ in 0..3 {
        send(message);
    }
}
Output
error[E0382]: use of moved value: `message`
 --> src/main.rs:8:14
  |
6 |     let message = String::from("heartbeat");
  |         ------- move occurs because `message` has type `String`, which does not implement the `Copy` trait
7 |     for _ in 0..3 {
  |     ------------- inside of this loop
8 |         send(message);
  |              ^^^^^^^ value moved here, in previous iteration of loop
  |
note: consider changing this parameter type in function `send` to borrow instead if owning the value isn't necessary
 --> src/main.rs:1:18
  |
1 | fn send(message: String) {
  |    ----          ^^^^^^ this parameter takes ownership of the value
  |    |
  |    in this function
help: consider cloning the value if the performance cost is acceptable
  |
8 |         send(message.clone());
  |                     ++++++++

Each call would need its own String. You could clone it per call, but the real fix is for send not to take ownership at all, since it only reads the message. That is the next lesson.

11. Consuming in one branch

Rust✗ does not compile · E0382
fn archive(record: String) {
    println!("archived: {record}");
}

fn main() {
    let record = String::from("invoice-2026-001");
    let is_old = true;
    if is_old {
        archive(record);
    }
    println!("still have {record}?");
}
Output
error[E0382]: borrow of moved value: `record`
  --> src/main.rs:11:27
   |
 6 |     let record = String::from("invoice-2026-001");
   |         ------ move occurs because `record` has type `String`, which does not implement the `Copy` trait
...
 9 |         archive(record);
   |                 ------ value moved here
10 |     }
11 |     println!("still have {record}?");
   |                           ^^^^^^ value borrowed here after move
   |
note: consider changing this parameter type in function `archive` to borrow instead if owning the value isn't necessary
  --> src/main.rs:1:20
   |
 1 | fn archive(record: String) {
   |    -------         ^^^^^^ this parameter takes ownership of the value
   |    |
   |    in this function
help: consider cloning the value if the performance cost is acceptable
   |
 9 |         archive(record.clone());
   |                       ++++++++

12. Giving back several values with a tuple

Before learning references, the only way to let a function look at a value and still keep it is to return it again, alongside the result.

Rust
fn count_words(text: String) -> (String, usize) {
    let count = text.split_whitespace().count();
    (text, count) // give the String back, plus the result
}

fn main() {
    let text = String::from("ownership is a set of rules");
    let (text, words) = count_words(text);
    println!("'{text}' has {words} words");
}
Output
'ownership is a set of rules' has 6 words

This works, but it is clumsy. Every function that only reads a value would have to return it. Rust has a much better answer, which is the point this lesson is building to.

13. You cannot return a reference to a local variable

What about returning just a pointer to text the function created? The local String is dropped when the function ends, so a reference to it would point at freed memory: a dangling reference. The compiler refuses.

Rust✗ does not compile · E0106
fn make_label() -> &String {
    let label = String::from("temporary");
    &label
}

fn main() {
    println!("{}", make_label());
}
Output
error[E0106]: missing lifetime specifier
 --> src/main.rs:1:20
  |
1 | fn make_label() -> &String {
  |                    ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
  |
1 | fn make_label() -> &'static String {
  |                     +++++++
help: instead, you are more likely to want to return an owned value
  |
1 - fn make_label() -> &String {
1 + fn make_label() -> String {
  |

The fix is to return the owned String itself, moving it to the caller, as in example 4. The error message talks about a "lifetime specifier"; lifetimes get a whole section later.

14. A function that stores its argument

When a function's job is to keep a value, taking ownership is exactly right. Here the registry is passed in and handed back, because we have not seen &mut yet.

Rust
fn register(mut registry: Vec<String>, service: String) -> Vec<String> {
    println!("registering {service}");
    registry.push(service);
    registry
}

fn main() {
    let registry = Vec::new();
    let registry = register(registry, String::from("auth"));
    let registry = register(registry, String::from("billing"));
    let registry = register(registry, String::from("search"));
    println!("{} services: {:?}", registry.len(), registry);
}
Output
registering auth
registering billing
registering search
3 services: ["auth", "billing", "search"]

15. Splitting ownership into two results

Rust
fn split_by_domain(emails: Vec<String>, domain: &str) -> (Vec<String>, Vec<String>) {
    let mut internal = Vec::new();
    let mut external = Vec::new();
    for email in emails {
        if email.ends_with(domain) {
            internal.push(email);
        } else {
            external.push(email);
        }
    }
    (internal, external)
}

fn main() {
    let emails = vec![
        String::from("ana@corp.com"),
        String::from("bob@gmail.com"),
        String::from("cy@corp.com"),
    ];
    let (internal, external) = split_by_domain(emails, "@corp.com");
    println!("internal: {:?}", internal);
    println!("external: {:?}", external);
}
Output
internal: ["ana@corp.com", "cy@corp.com"]
external: ["bob@gmail.com"]

16. Owned parameters with Copy parameters

A function can mix both kinds of parameters. Only the non-Copy ones move.

Rust
fn repeat_line(line: String, times: usize) -> String {
    let mut out = String::new();
    for i in 0..times {
        if i > 0 {
            out.push('\n');
        }
        out.push_str(&line);
    }
    out
}

fn main() {
    let count = 3;
    let block = repeat_line(String::from("-- cut here --"), count);
    println!("{block}");
    println!("count is still usable: {count}");
}
Output
-- cut here --
-- cut here --
-- cut here --
count is still usable: 3

17. Tuples as parameters move as a whole

Rust✗ does not compile · E0382
fn describe(product: (String, u32)) -> String {
    format!("{} (stock: {})", product.0, product.1)
}

fn main() {
    let product = (String::from("lamp"), 12);
    println!("{}", describe(product));
    println!("{}", product.1);
}
Output
error[E0382]: borrow of moved value: `product`
 --> src/main.rs:8:20
  |
6 |     let product = (String::from("lamp"), 12);
  |         ------- move occurs because `product` has type `(String, u32)`, which does not implement the `Copy` trait
7 |     println!("{}", describe(product));
  |                             ------- value moved here
8 |     println!("{}", product.1);
  |                    ^^^^^^^^^ value borrowed here after move
  |
note: consider changing this parameter type in function `describe` to borrow instead if owning the value isn't necessary
 --> src/main.rs:1:22
  |
1 | fn describe(product: (String, u32)) -> String {
  |    --------          ^^^^^^^^^^^^^ this parameter takes ownership of the value
  |    |
  |    in this function
help: consider cloning the value if the performance cost is acceptable
  |
7 |     println!("{}", describe(product.clone()));
  |                                    ++++++++

Even though product.1 is a u32, the tuple was moved as a whole into describe, so no part of it can be used afterwards.

18. Passing a clone to keep the original

When a function takes ownership but you still need the value, pass a clone. This is correct but costs a copy; the next lesson shows how to avoid it when the function only reads.

Rust
fn archive(record: String) {
    println!("archived: {record}");
}

fn main() {
    let record = String::from("invoice-2026-001");
    archive(record.clone());
    println!("still have {record}");
}
Output
archived: invoice-2026-001
still have invoice-2026-001

19. Which parameter type should a function take?

Look at what the function does with the value:

  • Keeps it (stores it in a collection, returns it, sends it elsewhere) → take ownership: fn store(s: String).
  • Only reads it → borrow: fn print(s: &str) or fn total(v: &Vec<i32>). Next lesson.
  • Changes it in place for the caller → borrow mutably: fn normalize(s: &mut String). The lesson after that.

This example shows the difference that borrowing makes. word_count only reads, so it borrows the text with &, and the caller keeps ownership:

Rust
fn word_count(text: &String) -> usize {
    text.split_whitespace().count()
}

fn main() {
    let text = String::from("borrowing lets functions look without owning");
    let n = word_count(&text);
    println!("'{text}' has {n} words");
}
Output
'borrowing lets functions look without owning' has 6 words

Compare it with example 12: no tuple, no giving back, no clone.

20. Realistic example: an order pipeline

Each stage takes ownership of the order and returns it, which models "this stage is now responsible for the order". The last stage moves the order into the database vector. An order is a tuple of (id, items, status).

Rust
fn create_order(id: u32, items: Vec<String>) -> (u32, Vec<String>, String) {
    (id, items, String::from("created"))
}

fn validate(order: (u32, Vec<String>, String)) -> (u32, Vec<String>, String) {
    let (id, items, _) = order;
    let status = if items.is_empty() { "invalid" } else { "validated" };
    (id, items, status.to_string())
}

fn add_gift_wrap(order: (u32, Vec<String>, String)) -> (u32, Vec<String>, String) {
    let (id, mut items, status) = order;
    items.push(String::from("gift wrap"));
    (id, items, status)
}

fn persist(
    mut database: Vec<(u32, Vec<String>, String)>,
    order: (u32, Vec<String>, String),
) -> Vec<(u32, Vec<String>, String)> {
    database.push(order);
    database
}

fn main() {
    let database = Vec::new();

    let order = create_order(1, vec![String::from("book"), String::from("pen")]);
    let order = validate(order);
    let order = add_gift_wrap(order);
    let database = persist(database, order);

    let empty = validate(create_order(2, Vec::new()));
    let database = persist(database, empty);

    for (id, items, status) in &database {
        println!("order {id}: {status} {:?}", items);
    }
}
Output
order 1: validated ["book", "pen", "gift wrap"]
order 2: invalid []

This code is correct, but the tuple types are long and repetitive. Structs fix that by naming the shape once.

21. Realistic example: deduplicating a list

Rust
fn dedupe(mut items: Vec<String>) -> Vec<String> {
    items.sort();
    items.dedup();
    items
}

fn main() {
    let tags = vec![
        String::from("rust"),
        String::from("java"),
        String::from("rust"),
        String::from("cli"),
        String::from("java"),
    ];
    let unique = dedupe(tags);
    println!("{:?}", unique);
}
Output
["cli", "java", "rust"]

22. Realistic example: an HTML builder

Each helper takes the document so far, appends to it, and returns it.

Rust
fn open_tag(mut html: String, tag: &str) -> String {
    html.push_str(&format!("<{tag}>"));
    html
}

fn text(mut html: String, content: &str) -> String {
    html.push_str(&content.replace('<', "&lt;"));
    html
}

fn close_tag(mut html: String, tag: &str) -> String {
    html.push_str(&format!("</{tag}>"));
    html
}

fn main() {
    let html = String::new();
    let html = open_tag(html, "p");
    let html = text(html, "Use Vec<T> for lists");
    let html = close_tag(html, "p");
    println!("{html}");
}
Output
<p>Use Vec&lt;T> for lists</p>

The &str parameters borrow text instead of taking ownership, and &format!(...) lends the formatted text to push_str. The next lesson explains both.

23. Realistic example: why "give it back" does not scale

A bank ledger that every operation must take and return. It works, but look at how every call has to thread the ledger through, even balance_of, which only reads.

Rust
fn deposit(mut ledger: Vec<(String, i64)>, account: &str, amount: i64) -> Vec<(String, i64)> {
    for entry in &mut ledger {
        if entry.0 == account {
            entry.1 += amount;
        }
    }
    ledger
}

fn balance_of(ledger: Vec<(String, i64)>, account: &str) -> (Vec<(String, i64)>, i64) {
    let mut balance = 0;
    for (name, amount) in &ledger {
        if name == account {
            balance = *amount;
        }
    }
    (ledger, balance) // must give the ledger back just to read one number
}

fn main() {
    let ledger = vec![(String::from("alice"), 100), (String::from("bob"), 50)];
    let ledger = deposit(ledger, "alice", 25);
    let ledger = deposit(ledger, "bob", 10);
    let (ledger, alice) = balance_of(ledger, "alice");
    let (_ledger, bob) = balance_of(ledger, "bob");
    println!("alice={alice} bob={bob}");
}
Output
alice=125 bob=60

With references, deposit would take &mut Vec<...> and return nothing, and balance_of would take &Vec<...> and return just the number. That is exactly where we go next.

24. Realistic example: a function that decides whether to keep a value

A function that takes ownership can decide what happens to the value: keep it, pass it on, or drop it. Here, a rate limiter keeps only requests within the limit.

Rust
fn admit(mut admitted: Vec<String>, request: String, limit: usize) -> Vec<String> {
    if admitted.len() < limit {
        admitted.push(request);
    } else {
        println!("rejected: {request}"); // `request` is dropped at the end of this function
    }
    admitted
}

fn main() {
    let mut admitted = Vec::new();
    for i in 1..=5 {
        let request = format!("GET /api/items?page={i}");
        admitted = admit(admitted, request, 3);
    }
    println!("admitted: {:?}", admitted);
}
Output
rejected: GET /api/items?page=4
rejected: GET /api/items?page=5
admitted: ["GET /api/items?page=1", "GET /api/items?page=2", "GET /api/items?page=3"]

Summary

  • Passing a non-Copy value to a function moves it. The caller cannot use it afterwards.
  • Returning a value moves it to the caller. Values created inside a function can be returned safely.
  • A function cannot return a reference to its own local variable: the local is dropped when the function returns.
  • Take ownership when a function keeps, returns, or passes on a value.
  • For functions that only read or modify a value, handing ownership in and out is clumsy. Borrowing is the solution.

Next: References & Borrowing.