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

Choosing Between String, &String, and &str in Function Interfaces

Hello! Last time, you used &str to borrow a whole string or a portion of one. Now the question is which type to put in a function signature. A function that reads a label, one that stores it, and one that returns a piece of it have different ownership needs—even though all three work with text.

By the end of this lesson, you should be able to choose among String, &String, and &str by asking what the function needs to do, rather than by matching the type of the caller’s variable. That distinction will be useful in the command-line text analyser later in this module and in API handlers further on.


Three types, two ownership roles

A String owns growable UTF-8 text. A &str is a borrowed view of UTF-8 text; it might view an entire String, part of one, or a string literal. A &String borrows a String value specifically, rather than borrowing its text as a slice.

let owned: String = String::from("  pending  ");
let borrowed_string: &String = &owned;
let borrowed_text: &str = owned.as_str();
let middle: &str = owned.trim();
let literal: &str = "pending";

Here, borrowed_string, borrowed_text, and middle all depend on owned. None owns a second copy of its text. literal is also a &str, but its bytes come from the program’s compiled-in literal rather than from owned.

Watch how Federico Terzi uses trim to make this distinction concrete. The key observation is that trimming can return a view into the existing text, while producing different text may require an owned result.

Understanding Strings in Rust - String vs &str

Watch “Understanding Strings in Rust - String vs &str” by Federico Terzi for its visual account of borrowing text and its initial advice on function signatures.

Watch trimming without copying, where the view covers fewer bytes than the original string. Then watch ownership conversions and function signatures. Treat the final advice as a starting rule, not an absolute one: a function can also return a borrowed &str when it returns part of its input.

The decision is not “heap versus stack” at every call site. For interfaces, the more useful questions are: Does this function only inspect text? Does it need to own text after the call? Is its result new text or a view of existing text?


Reading text: accept &str

Suppose a function counts the bytes in a label:

fn byte_count(label: &str) -> usize {
    label.len()
}

fn main() {
    let owned = String::from("queued");
    let literal = "ready";
    let prefix = &owned[..3];

    println!("{}", byte_count(&owned)); // 6
    println!("{}", byte_count(literal)); // 5
    println!("{}", byte_count(prefix));  // 3
}

One signature serves all three callers. Passing &owned works because Rust can coerce &String to &str where a &str is expected. No text is cloned or allocated for that conversion. You can write byte_count(owned.as_str()) instead if you want to make the slice explicit.

Notice that byte_count(owned) is not the call shown above: for this free function, supply a borrow with &owned. Also, .len() counts UTF-8 bytes, not necessarily visible characters, as in the previous lesson.

What if the parameter were &String?

fn byte_count_restricted(label: &String) -> usize {
    label.len()
}

It can accept &owned, but it cannot directly accept the literal "ready" or the partial slice prefix. The function needs only the text, yet its signature demands that the caller have a String to borrow. Converting a literal into a String just to call it would add an unnecessary allocation.

The Rust Book makes this comparison using first_word. You saw that function last time; here, concentrate on how changing its parameter broadens the callers it accepts.

The Slice Type - The Rust Programming Language

Read the “String Slices as Parameters” subsection of the official Rust Book to see why &str is usually the more useful read-only parameter type.

In “String Slices as Parameters,” begin after the initial fn first_word(s: &String) -> &str signature, at the signature comparison. Stop before “Other Slices.” Focus on the calls using a full String, a partial slice, and a literal, then on the explanation of deref coercion. You already know how first_word finds its slice; there is no need to rework that scan.

When is &String appropriate? Rarely, but it is not invalid. A function might specifically need a property of the owning String that a text slice does not expose—for example, its allocated capacity:

fn spare_capacity(text: &String) -> usize {
    text.capacity() - text.len()
}

That function is asking about the String allocation, not merely its text. &String still grants read-only access; it does not let the function append characters. For ordinary inspection, searching, parsing, or formatting, prefer &str.


Keeping text: accept String

Reading text during a call is different from keeping it after the call. Consider a record whose label must remain available independently of the caller’s variable:

struct Record {
    label: String,
}

fn make_record(label: String) -> Record {
    Record { label }
}

fn main() {
    let label = String::from("transfer");
    let record = make_record(label);

    println!("{}", record.label);
    // println!("{label}"); // Would not compile: ownership moved to make_record.
}

The String parameter says the function takes ownership. Here, ownership passes into the Record. Moving the String does not, by itself, copy all its text into a new heap allocation; it transfers responsibility for the existing owned value. The caller can no longer use label after that move.

A caller holding only a &str has no owned String to transfer. It can explicitly make one:

let record = make_record("transfer".to_owned());

This conversion allocates and copies the text because the new record needs its own owned value. The same ownership requirement is why String::from("transfer") is also suitable here.

A useful design rule is therefore:

  • Only inspect text during the call: accept &str.
  • Keep the supplied owned text: accept String and let the caller move it in or explicitly create an owned copy.
  • Inspect the String object itself, rather than just its text: consider &String.

Taking String is not required merely because a function produces different text. It can borrow its input and return a newly owned result:

fn display_label(input: &str) -> String {
    format!("Label: {}", input.trim())
}

input.trim() is a borrowed view. format! builds new text, so the result is a String. Both display_label(" ready ") and display_label(&some_string) work.


Returning text: new value or borrowed view?

The same distinction guides return types. If the function constructs text that must survive independently of its inputs, return String, as display_label does. If it selects part of an input, it can return &str:

fn without_padding(input: &str) -> &str {
    input.trim()
}

fn main() {
    let message = String::from("  approved  ");
    let cleaned = without_padding(&message);

    println!("{cleaned}"); // approved
}

cleaned borrows from message; it is not an independently owned string. Rust therefore will not let you keep using cleaned after dropping or changing message in a way that would invalidate that borrow.

By contrast, a function cannot safely return a slice into a String it creates locally:

// Does not compile:
fn broken() -> &str {
    let local = String::from("temporary");
    &local
}

local would be dropped when the function returns, leaving the slice pointing at text that no longer exists. If the caller needs that newly created text, return the String itself. This is the limitation behind the video’s practical advice that functions often take &str and return String—but returning &str is right when the result borrows from an input that remains alive.

Putting the choices together gives a small text-processing pipeline:

struct Record {
    label: String,
}

fn without_padding(input: &str) -> &str {
    input.trim()
}

fn describe(input: &str) -> String {
    format!("Label: {input}")
}

fn make_record(label: String) -> Record {
    Record { label }
}

fn main() {
    let raw = String::from("  transfer  ");
    let cleaned = without_padding(&raw); // Borrows part of raw
    let description = describe(cleaned); // Builds new, owned text
    let record = make_record(description); // Moves ownership into record

    println!("{}", record.label); // Label: transfer
    println!("{raw}");              // raw was only borrowed, not moved
}

Each signature states a distinct contract. without_padding returns a view tied to its input. describe reads a view but creates new text. make_record takes an owned value because the record keeps it. The caller’s original raw remains usable throughout because none of those calls takes ownership of it.


Key takeaways

Use &str for functions that need to read text: it accepts literals, slices, and borrows of String without copying. Use String when a function must take ownership of supplied text. Reserve &String for the uncommon case where the function needs to inspect the owning String itself, not just its contents. For results, return String for independently created text and &str for a view borrowed from an input.

Next, you’ll put these ownership choices to work when storing and retrieving values in Vec and HashMap.

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

Sign up