Question

Rust String vs str: Differences, Uses, and When to Choose Each

ruststring

Question

In Rust, why are there both String and str types? What is the difference between them, when should each one be used, and is either type being deprecated?

Short Answer

By the end of this page, you will understand how Rust represents text with both String and str, why they are different types, and how ownership and borrowing affect string handling. You will also learn when to use String, when to use &str, and why neither type is being deprecated.

Concept

Rust has multiple string-related types because they serve different purposes in Rust's ownership system.

  • String is an owned, growable UTF-8 text buffer.
  • str is a string slice type, usually used as &str, which is a borrowed view into UTF-8 text.

Why Rust needs both

In Rust, ownership matters. Sometimes your code needs to own text so it can store it, modify it, or return it from a function. Other times, your code only needs to read some text temporarily without taking ownership.

Having separate types makes these cases explicit:

  • Use String when your code owns the text.
  • Use &str when your code just borrows text.

This helps Rust prevent memory errors and unnecessary copying.

String

String stores text on the heap and owns that memory. Because it owns the data, it can:

  • grow or shrink
  • be modified
  • be moved between variables
  • be returned from functions as owned data

Example:

let mut name = String::from("Rust");
name.push_str(" language");

str

str is not usually used by itself. You normally see it as &str.

A &str is a reference to UTF-8 text stored somewhere else. That text might come from:

  • a string literal
  • part of a String
  • part of another &str

Example:

let language: &str = "Rust";

String literals like "Rust" have type &'static str, meaning they are stored in the program binary and live for the whole program.

Important relationship

A String can be borrowed as &str:

let s = String::from("hello");
let slice: &str = &s;

This is why many Rust APIs accept &str: they can work with both string literals and String values without forcing ownership.

Is one deprecated?

No. Neither String nor str is deprecated.

They are both fundamental parts of Rust:

  • String is the standard owned string type.
  • str is the fundamental string slice type.

Both are actively used throughout the language and standard library.

Mental Model

Think of String as a book you own, and &str as a page reference or excerpt from a book.

  • If you own the whole book and can add or remove pages, that is like String.
  • If you are only pointing to some text inside a book owned elsewhere, that is like &str.

Another way to see it:

  • String = the actual storage container
  • &str = a window looking into text

The window does not own the text. It only lets you read it while the original text still exists.

Take Quiz

Syntax and Examples

Core syntax

let owned: String = String::from("hello");
let borrowed: &str = "hello";

Create a String

let a = String::from("hello");
let b = "hello".to_string();

Both create owned strings.

Use a string slice

let name: &str = "Ada";

This borrows a string literal.

Borrow a String as &str

let message = String::from("welcome");
let view: &str = &message;

Here, view does not own the text. It just points to the contents of message.

Function parameters

A common beginner-friendly pattern is to accept &str when you only need to read text:

fn greet(name: &str) {
    println!("Hello, {}!", name);
}

fn main() {
    let a = "Rust";
    let b = String::from("Ferris");

    greet(a);
    greet(&b);
}

This is flexible because both string literals and String values can be used.

When you need ownership

fn make_message(name: &str) -> String {
    format!("Hello, {}!", name)
}

This function returns a new owned String because it creates new text.

Slicing text

let word = String::from("hello");
let part: &str = &word[0..2];
println!("{}", part);

This creates a string slice referencing part of the string.

Be careful: Rust strings are UTF-8, so not every byte range is a valid character boundary.

Step by Step Execution

Consider this example:

fn print_length(text: &str) {
    println!("{} has length {}", text, text.len());
}

fn main() {
    let s = String::from("hello");
    let slice = &s;
    print_length(slice);
}

Step by step:

  1. String::from("hello") creates an owned String on the heap.
  2. s owns that string data.
  3. let slice = &s; borrows s as &str.
  4. slice does not copy the text. It only points to the same text.
  5. print_length(slice) passes that borrowed view into the function.
  6. Inside print_length, the function can read the text but does not own it.
  7. When the function ends, slice is still valid because s still exists.
  8. At the end of main, s is dropped, and its memory is freed.

What if the function took String instead?

fn print_length(text: String) {
    println!("{} has length {}", text, text.len());
}

Then calling print_length(s) would move ownership into the function. After that, s could no longer be used unless cloned. That is why &str is often the better parameter type for read-only text.

Real World Use Cases

Accepting user input

When reading input from a user, you often store it in a String because the program owns that text and may modify it.

let mut input = String::new();

API and function parameters

Functions that only read text commonly accept &str:

fn is_valid_email(email: &str) -> bool {
    email.contains('@')
}

This works with both literals and String values.

Building output text

When constructing messages, file contents, SQL queries, or JSON snippets, you usually return or build a String.

Parsing and processing text

Parsers often receive &str because they inspect text without needing to own it.

Configuration values

A config loader may temporarily borrow text as &str while validating, then store selected values as String if they must live independently.

Take Quiz

Real Codebase Usage

In real Rust codebases, developers follow a few common patterns.

Prefer &str for read-only parameters

fn log_message(message: &str) {
    println!("LOG: {}", message);
}

This avoids unnecessary allocations and makes the function easier to call.

Use String for stored data

Struct fields often use String when the struct must own the text:

struct User {
    username: String,
    email: String,
}

Convert at boundaries

A common pattern is:

  • accept &str
  • validate or inspect it
  • convert to String only if ownership is needed
fn create_user(username: &str) -> User {
    User {
        username: username.to_string(),
        email: String::from("unknown@example.com"),
    }
}

Guard clauses and validation

fn save_name(name: &str) -> Result<String, String> {
    if name.trim().is_empty() {
        return Err(String::from("name cannot be empty"));
    }

    Ok(name.trim().to_string())
}

Here &str is used for input, and String is used for owned output and error messages.

Avoid cloning unless necessary

Developers try to borrow with &str instead of creating a new String unless they really need ownership.

Common Mistakes

Mistake 1: Thinking str and &str are the same thing

str is the slice type itself, but you almost always work with it through a reference: &str.

Broken idea:

let text: str = "hello";

Correct:

let text: &str = "hello";

Mistake 2: Using String for every function parameter

Broken:

fn greet(name: String) {
    println!("Hello, {}", name);
}

This forces callers to give ownership.

Better:

fn greet(name: &str) {
    println!("Hello, {}", name);
}

Mistake 3: Forgetting that String can be borrowed as &str

let name = String::from("Rust");
greet(&name);

You usually do not need to convert a String into another String just to pass it.

Mistake 4: Slicing UTF-8 strings at invalid byte boundaries

Broken:

let s = "é";
let part = &s[0..1];

This panics because é uses more than one byte in UTF-8.

Safer approaches include iterating over .chars() when you need character-based logic.

Mistake 5: Returning &str when the data is created inside the function

Broken:

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

This returns a reference to data that will be dropped.

Correct:

fn make_text() -> String {
    String::from("hello")
}

Comparisons

ConceptString&str
OwnershipOwns the textBorrows the text
SizeGrowableFixed view into existing text
MutabilityCan be modified if variable is mutCannot modify underlying text through &str
AllocationUsually heap-allocatedDoes not allocate by itself
Common useStore, build, return textRead-only parameters and views
Typical function argumentLess flexibleMore flexible

String vs &str in function design

Function signatureWhen to use it
fn f(text: &str)When you only need to read text
fn f(text: String)When the function must take ownership
fn f() -> StringWhen the function creates and returns new text

String vs string literal

A string literal like "hello" is not a String.

  • Literal type: &'static str
  • Owned heap string: String

You can convert a literal into a String when needed:

let s = "hello".to_string();
Take Quiz

Cheat Sheet

Quick rules

  • String = owned, growable UTF-8 text
  • str = string slice type
  • &str = borrowed view into UTF-8 text
  • String literals are usually &'static str
  • Neither String nor str is deprecated

Most common choices

fn read_only(text: &str) {}
fn creates_text() -> String { String::from("hi") }

Convert between them

let s: String = String::from("hello");
let slice: &str = &s;

let owned: String = slice.to_string();

When to use what

  • Use &str for function inputs when you only read text
  • Use String for stored fields, owned results, and editable text

Common constructors

String::from("hello")
"hello".to_string()

Important note about slicing

Rust strings are UTF-8. Byte indices must land on valid character boundaries.

Good default design advice

  • Accept &str
  • Return String when producing new text
  • Store String when ownership is required

FAQ

Why does Rust have both String and str?

Because Rust separates owned data from borrowed data. String owns text, while &str borrows text.

Should I use String or &str for function parameters?

Usually &str, unless the function must take ownership of the text.

Is str ever used without &?

Rarely in everyday Rust code. Most of the time you use &str.

Are string literals String values?

No. String literals are &'static str.

Is String just a wrapper around str?

You can think of String as owned string storage that can be borrowed as &str, but they are distinct types with different roles.

Is either String or str deprecated in Rust?

No. Both are core parts of Rust and are widely used.

Why do many Rust APIs accept &str?

Because it is more flexible and avoids forcing allocation or ownership transfer.

Can I modify a &str?

Not directly. If you need editable owned text, use String.

Take Quiz

Mini Project

Description

Build a small Rust program that manages a display name. This project demonstrates when to accept &str, when to store String, and how to convert borrowed text into owned text only when needed.

Goal

Create a program that validates a name from borrowed input and stores it as owned text in a struct.

Requirements

[ "Create a struct that stores a user's display name as a String.", "Write a function that accepts &str input and rejects empty or whitespace-only names.", "Trim valid input and store the cleaned value as a String.", "Print the saved display name to confirm the result." ]

Take Quiz

Keep learning

Related questions

View more