Rust - The differences between the &str and String types
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 &str → String 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
- Take
&strin function parameters (most flexible). - Store
Stringin structs you own (avoids lifetime complexity). - Return
Stringwhen you allocate/build new text.