Skip to content

Rust - The differences between the &str and String types

snow covered mountain in reflective photography

Photo by Kurt Cotoaga

Rust has two string types you’ll use constantly: String (owned, growable text) and &str (a borrowed slice of UTF‑8). This post explains when to use each one, focusing on ownership and allocation instead of the full memory layout.

The Problem

You have finally decided to write your first Rust program and become a Rustacean like the rest of us.

You decide to write a simple program, that says “Good morning!” to the user, it cannot be that difficult right? You just need a function to print that.

You are used to writing JavaScript with some TypeScript, so you know you need a function that takes as a parameter a string and concatenates that param with your “Good Morning, {name}”. After checking the Rust docs, you see that there is a parameter called String that seems perfect for your usecase.

So, you go ahead and open your text editor (Neovim) and write this simple program:

fn main() {
    let name = "John";
    good_morning(name);
}

fn good_morning(name: String) {
   println!("Good Morning, {name}");
}

You go to your terminal and try to run this program with cargo run but it doesn’t compile.

Why is that?

The Rust compiler tells us exactly what’s wrong:

error[E0308]: mismatched types
 --> src/main.rs:3:18
  |
3 |     good_morning(name);
  |     ------------ ^^^^- help: try using a conversion method: `.to_string()`
  |     |            |
  |     |            expected struct `String`, found `&str`
  |     arguments to this function are incorrect
  |
note: function defined here
 --> src/main.rs:6:4
  |
6 | fn good_morning(name: String) {
  |    ^^^^^^^^^^^^ ------------

For more information about this error, try `rustc --explain E0308`.

It is saying that it was expecting a string of type String but that we passed something of type &str.

But what the heck is that? Your variable name seems to be a perfectly normal string, so why isn’t the compiler happy?

Well, let’s try to understand what is going on.

The String type

The String type is conceptually very similar to the string type in JavaScript/TypeScript and other dynamic programming languages.

An element of type String can do almost all string operations that we find in other languages.

Strings are always valid UTF-8 and can grow in size, meaning they are perfect if you need to manipulate dynamic strings.

There are 3 main ways to create a String in Rust:

let x = String::from("Hi there!");
let y = "Hi there!".to_string();
let z = "Hi there!".to_owned();

As mentioned above, String types are growable meaning that you can append a char or a &str to it:

// Note that we need the `mut` keyword here
// to indicate that this is a mutable variable
let mut x = String::from("Foo");
println!("{x}");
// prints:
// Foo

// In Rust, to represent a `char`, you need to
// put it inside single quotes.
x.push('!');
println!("{x}");
// prints:
// Foo!

x.push_str("Bar");
println!("{x}");
// prints:
// Foo!Bar

As we can see, a String is flexible and it shines when we need to store text that is unknown at compile time or that needs to be manipulated.

These properties are directly linked to how String is stored in our computer’s memory: A String is heap-allocated and it’s stored as a vector of bytes (Vec<u8>).

In Rust, when you define a string only with double quotes, you are actually defining a string slice of type &str and not a String.

So, you might be wondering how we could fix our good_morning function to make it work with a String, we need to do it like that:

fn main() {
    let name = String::from("John");
    good_morning(name);
}

fn good_morning(name: String) {
   println!("Good Morning, {name}");
}

But ok, if Strings are heap-allocated and growable, what is &str?

The &str type

&str (a “string slice”) is an immutable, borrowed view into UTF-8 string data. It does not own the bytes and it can’t grow.

Technically, a &str is a fat pointer (pointer + length). The length is tracked at runtime. String literals like "Rust is awesome" have type &'static str, and their length is known at compile time.

&str is often more efficient because it can borrow existing data and doesn’t allocate. Use String when you need owned, growable text.

All string literals in Rust (e.g., "Rust is awesome") are of type &str, meaning that to create a &str you just need to write the text between double quotes:

let s = "Rust is awesome";

// type is `&str`

Because &str is an immutable borrowed view, you cannot append chars or &str to it. In other words, it can’t be modified.

If you need to modify a &str, you need to first convert it to String:

let s = "Rust is awesome";

	// we could have directly used the method `to_string` in the line just above
	// but bear with me for the example
let mut s_converted = String::from(s);
s_converted.push('!');

println!("{s_converted}");
// prints: Rust is awesome!

String slices are also useful when you want a reference to a String that is “owned” by someone else but you don’t want to copy/clone the string and allocate more memory to use it. If we go back to our good_morning function, we can see how this pattern can be useful:

fn main() {
    let name = String::from("John");
    // Here we are now passing a 'reference' (that is what the '&' means)
    // to our `good_morning` function
    // This compiles thanks to deref coercion (`&String` → `&str`).
    good_morning(&name);
}

// The function now takes as a param `&str`
fn good_morning(name: &str) {
   println!("Good Morning, {name}");
}

When passing strings between functions, &str is often the preferred type, as it accepts both string literals and String without forcing an allocation.

Another difference is that &str contains a pointer to the start of the string and the length of the string/slice. This can point to anything, like something in the stack, in the heap, static memory and etc. A String as mentioned before is heap allocated and dynamically allocated.

That leads to a practical rule: converting String&str is cheap (a borrow), but converting &strString requires a heap allocation.

What are the use cases for String and &str?

As a rule of thumb, I always try to use &str when the string is known at compile time and I don’t need to mutate or own it. A good example is our good_morning function.

But let’s say that our good_morning function parameter name is not coming from our code but from an external API that gets user input from a web page. In this case, we know that name cannot be known at compile time and in this case we need to use String.

Another good use case for String is when you need to mutate and manipulate the string.

Rules of thumb