Rust - The differences between the &str and String types

Photo by Kurt Cotoaga
For a first Rust program, a function that says “Good morning!” seems straightforward. Coming from JavaScript and TypeScript, you’d expect to take a string parameter and combine it with the greeting, “Good Morning, {name}”.
You check the Rust docs, find a type called String, and open Neovim to write the program:
fn main() {
let name = "John";
good_morning(name);
}
fn good_morning(name: String) {
println!("Good Morning, {name}");
}
Running it with cargo run produces a type-mismatch error. The exact diagnostic formatting depends on the compiler version:
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`.
The function expects a String, but name has type &str. In JavaScript, that distinction wouldn’t arise. In Rust, String owns text and can grow, while &str borrows a slice of UTF‑8. You’ll use both constantly, and the choice usually comes down to ownership and allocation.
The String type
Rust’s String supports most of the string operations familiar from JavaScript, TypeScript, and other dynamic languages. It always contains valid UTF-8 and can grow, which makes it suitable for text you need to manipulate.
Three common ways to create a String are:
let x = String::from("Hi there!");
let y = "Hi there!".to_string();
let z = "Hi there!".to_owned();
You can append a char or a &str to a mutable String:
// 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
This makes String useful when you need to own text or change its length. Its buffer is a vector of bytes (Vec<u8>) with room to grow on the heap. An empty String::new() starts without allocating a buffer.
A string literal in double quotes gives us a slice of type &str. To pass an owned String to good_morning, we can construct one explicitly:
fn main() {
let name = String::from("John");
good_morning(name);
}
fn good_morning(name: String) {
println!("Good Morning, {name}");
}
The &str type
&str (a “string slice”) is an immutable, borrowed view into UTF-8 string data. It doesn’t own the bytes and can’t grow.
A &str is a fat pointer: a pointer paired with a length in bytes, not a count of characters.
String literals like "Rust is awesome" have type &'static str, and their length is known at compile time.
Writing text between double quotes is enough to create one:
let s = "Rust is awesome";
// type is `&str`
You can’t append through a &str. To add an exclamation mark here, make an owned String and grow that buffer:
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!
Borrowing also lets us use a String owned by another part of the program without copying or cloning its data and allocating more memory. That often makes &str more efficient. Use String when you need owned, growable text; our good_morning function only needs to read the name, so it can borrow it:
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}");
}
The same parameter now accepts both string literals and borrows of a String without copying the text. Passing &name also leaves ownership with the caller; the earlier String parameter moved it into the function. The Rust book’s slice example uses the same approach for functions that only read text.
A slice’s pointer and length can refer to string data on the stack, the heap, or in static memory. A String owns its buffer; a slice borrows bytes that must remain valid for as long as the slice is used.
The direction of the conversion matters: borrowing a String as &str doesn’t allocate. Creating a String from a nonempty &str allocates a buffer and copies the bytes. The empty case needs no buffer.
Choosing between String and &str
I use &str when a function only needs to read text that another part of the program owns. The good_morning function is one example. Whether that text was known at compile time doesn’t decide the parameter type.
If name comes from user input, a String can hold it, and good_morning can still borrow it as &str. Some parsers can also return slices into an existing input buffer, without creating a separate String for every field.
Use String when you need ownership or a buffer that can grow. A mutable slice (&mut str) permits some changes in place, such as ASCII case conversion, but can’t change the slice’s length. For function and struct design, the usual choices are:
- Take
&strwhen a function only needs to read existing text. - Store
Stringin structs you own (avoids lifetime complexity). - Return
Stringwhen you allocate/build new text.