8.6Ownership and functions

Last updated June 12, 2026

Lesson 2.3 owes this course's readers the second half of an asterisk: "'Arguments are copied' is the whole truth for integers and the other simple types in these chapters, and a simplification for types like String. The full story is the story of this course: ownership, chapter 8." Last lesson paid the Copy half. Today: what actually happens when a String meets a function.

Passing gives

You can likely predict it now. Function calls hand values to parameters the same way = hands values to variables, so the rules are lesson 8.4's rules: Copy types copy, owners move. Passing a String to a function gives it away:

fn main() {
    let order = String::from("two coffees");
    submit(order);
    println!("{order}");
}

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

The anatomy is last lesson's friend E0382 with one new organ. The top half you can read fluently now: the move-because-String note, value moved here at the call, the illegal use after. The new part is the middle note, and it's remarkable: the compiler walks into submit's definition, points at the parameter type, and says this parameter takes ownership of the value. The phrase "takes ownership" is the lesson, straight from the diagnostic: a String parameter doesn't receive a copy, it receives the value itself, and the caller's variable is left moved-from.

And look at the compiler's first suggestion: change the parameter "to borrow instead." That word is chapter 9's entire subject. When the diagnostics start advertising the next chapter, you're reading the course in the right order.

What happens to order's value if the call is legal (drop the fourth line)? text owns it now. text is a local of submit, so at submit's closing brace, the value is dropped, heap block returned (lesson 8.3). Handing a value to a function really is giving it away: by default, the function keeps it, uses it, and disposes of it.

Meanwhile, for Copy types nothing changed all chapter:

fn main() {
    let count = 12;
    report(count);
    println!("still here: {count}");
}

fn report(n: u32) {
    println!("counted {n}");
}
counted 12
still here: 12

Lesson 2.3's asterisk is now fully cashed: arguments are copied when the type is Copy, and moved when it isn't. That is the complete truth, no asterisks remaining.

Returning gives back

Moves flow out of functions too. A return (or tail expression) moves the value to the caller, which is the proper description of a function you wrote in lesson 5.x's capstone:

use std::io;

fn read_name() -> String {
    println!("Name?");
    let mut input = String::new();
    io::stdin()
        .read_line(&mut input)
        .expect("failed to read input");
    String::from(input.trim())
}

fn main() {
    let name = read_name();
    println!("hello, {name}");
}
Name?
hello, Ada

In chapter 5 the justification was intuition: "the caller is keeping the text, so a view of the function's local input wouldn't survive." Now every word has machinery under it. input is owned by read_name and dies at its closing brace; a view into it could outlive the text (chapter 9 makes that a compile error, as promised). But String::from(input.trim()) builds a new owned value, and the tail expression moves it out to main, where name becomes its owner. Ownership flowed in neither direction by copy: the function built a value and gave it to its caller.

This also settles a chapter 5 style rule properly. Lesson 5.4 said String parameters are for functions that keep the text. Now you can say it with this chapter's vocabulary: a String parameter means "I take ownership"; a &str parameter means "I only look." Functions that merely read text should ask for the view. Functions that will own it (store it, consume it, return it transformed) ask for the String, and the move at the call site is the honest record of that transfer.

The give-and-take-back dance

But what if a function needs to use a String and the caller needs it afterwards? With only this chapter's tools, the function must hand it back, and since it also wants to return its actual result, you reach for lesson 4.11's tuples:

fn measure(text: String) -> (String, usize) {
    let bytes = text.len();
    (text, bytes)
}

fn main() {
    let name = String::from("Ada Lovelace");
    let (name, bytes) = measure(name);
    println!("{name} is {bytes} bytes long");
}
Ada Lovelace is 12 bytes long

It compiles, it runs, and it is awkward on purpose. Look at the bookkeeping: the function's real job returns one usize, but its signature drags the String along like a deposit being refunded, and the caller re-binds name (a shadowing let, lesson 5.2) just to receive its own value back. Imagine three string parameters. Imagine doing this in every function in a real program.

Rust programmers do not live like this, and the compiler already told you the way out, in this lesson's first error message: "consider changing this parameter type to borrow instead." You have also been using the answer since lesson 1.6: every read_line(&mut name) you've ever written lends name to a function that modifies it and gives it back automatically, no tuple, no re-binding. That & is a reference, the act is borrowing, and chapter 9 is where it finally earns its name. This lesson's dance exists so you'll know exactly what references are saving you from.

Best practice

Parameters: take &str when the function only reads the text; take String only when it genuinely keeps ownership. Returns: handing a newly built String to the caller is exactly right (that's read_name, format!, and friends). The give-and-take-back tuple is a teaching device; in real code, chapter 9's references do this job.

An old debt: the consuming +

One last IOU, from lesson 5.3: "+ consumes the String on its left side, which stops compiling in ways we can't explain until chapter 8 puts ownership on the table."

The table is set. Concatenation with + is, under the hood, a function call, and its left operand is a String parameter: it takes ownership. The right side is only lent (that & again). So:

fn main() {
    let first = String::from("Ada");
    let last = String::from("Lovelace");
    let full = first + " " + &last;
    println!("{full}");
    println!("{last}");
}
Ada Lovelace
Lovelace

last survives: it was only viewed. first is gone: it was given to the +, exactly like order was given to submit, and adding println!("{first}") would produce the same E0382 you can now read in your sleep. (Why design + to consume? Because owning the left side lets it grow that existing heap block and hand it onward, instead of copying all the text into a new one.) Chapter 5's advice stands with reasons attached: push_str modifies what you keep, format! borrows everything and consumes nothing (lesson 5.5), and + is fine when you're done with the left side.

Quiz time

Question #1

Compile or not? One sentence of why.

fn shout(line: String) {
    println!("{line}!!!");
}

fn main() {
    let cheer = String::from("goal");
    shout(cheer);
    shout(cheer);
}
Show solution

error[E0382]: use of moved value: cheer``. The first call moves the value into line, which drops it when shout returns; the second call tries to move a value that no longer exists. (Fixes, in chapter order: build a second String, clone for the first call, or wait one chapter and lend it twice.)

Question #2

Without references (chapter 9 is still off-limits), fix this program so it compiles and prints both lines, keeping describe's ability to read the text:

fn describe(text: String) -> usize {
    text.len()
}

fn main() {
    let motto = String::from("fearless");
    let n = describe(motto);
    println!("{motto}: {n} bytes");
}
Show solution

Make describe give the value back alongside its answer:

fn describe(text: String) -> (String, usize) {
    let n = text.len();
    (text, n)
}

fn main() {
    let motto = String::from("fearless");
    let (motto, n) = describe(motto);
    println!("{motto}: {n} bytes");
}
fearless: 8 bytes

The tuple return moves ownership back out, and the shadowing let (motto, n) receives it. Feel the friction; it's the lesson. (A .clone() at the call site also works at the cost of duplicating the text, and chapter 9 deletes the whole problem.)

Question #3

For each function, pick the parameter type, String or &str, and justify it in ownership words: a) fn print_banner(title: ???) which decorates and prints the title; b) fn archive(entry: ???) which stores the text in a long-lived archive; c) fn first_word(sentence: ???) -> ??? which returns the word before the first space.

Show solution

a) &str: the function only reads; it has no business taking ownership it doesn't need. b) String: the archive keeps the text, so taking ownership is honest, and the move at the call site documents the handoff. c) Takes &str, and (with this chapter's tools) returns a new String built from the word: the function reads its input and gives the caller ownership of a fresh value. (Chapter 9 will offer a better return type: a view into the original sentence.)

One tool remains in the chapter: the compiler has suggested .clone() at you twice now. Next lesson it gets a fair hearing, including when it's the right call and when it's a crutch.