String and &str in Rust
Editor’s note: This article was last updated by Joseph Mawa on 26 March 2024 to include information about string operations in Rust, such as string slicing and pattern matching using the contains, starts_with, and find methods. It also now covers string conversions, including converting to strings and parsing strings.
Depending on your programming background, Rust’s multiple string types, such as String and str, can be confusing. In this post, we will clarify the differences between String and str, or more accurately, between String, &String, and &str — especially when deciding which type to use.
Understanding the concepts in this post will enable you to use strings in Rust more efficiently, as well as better understand other people’s code when it comes to the thought process behind handling strings.
First, we will explore the more theoretical aspects, such as the different structures of the string types and their differences regarding mutability and memory location. Then, we will look at the practical implications of these differences, discussing when to use which type. Finally, we will cover some simple code examples showcasing the different ways of using strings in Rust.
If you’re just getting started with Rust, this post might be useful to you. It will explain why sometimes your code doesn’t compile when you work with strings. However, even if you’re not entirely new to Rust, strings are of fundamental relevance to many different areas of software engineering — having at least a basic understanding of how they work in the language you’re using is important.
Let’s get started!
The Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
In this section, we’ll explore the differences between the types at the language level and their implications.
Generally speaking, the difference comes down to ownership and memory. All string types in Rust are always guaranteed to be valid UTF-8.
String?String is an owned type that needs to be allocated. It has dynamic size and therefore, its size is unknown at compile time, as the capacity of the internal array can change at any time. The type itself is a struct of the form:
pub struct String {
vec: Vec<u8>,
}
Because it contains a Vec, we know that it has a pointer to a chunk of memory, a size, and a capacity. The size gives us the length of the string and the capacity tells us how long it can get before we need to reallocate. The pointer points to a contiguous char array on the heap of capacity length and size entries in it.
Strings are very flexible. We can always create new, dynamic strings with them and mutate them. But this comes at a cost — we need to always allocate new memory to create them.
&String?The &String type is a reference to a String. This means that this isn’t an owned type and its size is known at compile time because it’s only a pointer to an actual String.
There isn’t much to say about &String that hasn’t already been said about String. But because it’s not an owned type, we can pass &String around, as long as the thing we’re referencing doesn’t go out of scope, and we don’t need to worry about allocations.
Mostly, the implications between String and &String comes down to references and borrowing.
An interesting aspect of &String is that it can be Deref coerced to &str by the Rust compiler. This is great in terms of API flexibility. However, this does not work the other way around:
fn main() {
let s = "hello_world";
let mut mutable_string = String::from("hello");
coerce_success(&mutable_string);
coerce_fail(s);
}
fn coerce_success(data: &str) { // compiles just fine, even though we put in a &String
println!("{}", data);
}
fn coerce_fail(data: &String) { // error - expected &String, but found &str
println!("{}", data);
}
&str?Finally, let’s look at &str. Because &str consists of just a pointer into memory (as well as a size), its size is known at compile time.
The memory can be on the heap, stack, or static directly from the program executable. It’s not an owned type, but rather a read-only reference to a string slice. Rust guarantees that while the &str is in scope, the underlying memory does not change, even across threads.
As mentioned above, &String can be coerced to &str, which makes &str a great candidate for function arguments, if mutability and ownership are not required.
&str is best used when a slice (view) of a string is needed, which does not need to be changed. However, keep in mind that &str is just a pointer to a str, which has an unknown size at compile time, but is not dynamic in nature, so its capacity can’t be changed.
This is important to know, but as mentioned above, the memory that the &str points to cannot be changed while the &str is in existence, even by the owner of str.
Let’s look at some of the practical implications of this.
&strUnderstanding the concept above will make you write better code that the Rust compiler will happily accept. And beyond that, there are some implications in terms of API design, as well as in terms of performance, to consider.
In terms of performance, you have to consider that creating a new String always triggers an allocation. If you can avoid additional allocations, you should always do so, as they take quite some time and will take a heavy toll on your runtime performance.
Consider a case where, within a nested loop, you always need a different part of a certain string in your program. If you create a new String sub-strings every time, you would have to allocate memory for each of the sub-strings and do much more work than you would need to because you could just use a &str string slice, a read-only view of your base String.
The same goes for passing around data in your application. If you pass around owned String instances instead of mutable borrows (&mut String) or read-only views of them (&str), many more allocations will be triggered and much more memory has to be kept around.
So in terms of performance, knowing when an allocation happens and when you don’t need one is important as you can re-use a string slice when necessary.
When it comes to API design, things become a bit more involved, as you will have certain goals with your API in mind, and choosing the right type is very important for consumers of your API to be able to reach the goals you have for them.
For example, because &String can be coerced to &str, but not the other way around, it usually makes sense to use &str as a parameter type, if you just need a read-only view of a string.
Furthermore, if a function needs to mutate a given String, there is no point in passing in a &str, as it will be immutable and you would have to create a String from it and return it afterward. If mutation is what you need, use &mut String.
The same goes for cases where you need an owned string, for example for sending it to a thread, or for creating a struct that expects an owned string. In those cases, you will need to use String directly, as both &String and &str are borrowed types.
In any case, where ownership and mutability are important, the choice between String and &str becomes important. In most cases, the code won’t compile if you’re not using the correct one, but if you don’t have a good understanding of which type has which properties and what happens during conversion from one type to the other, you might create confusing APIs that do more allocations than they need to.
Let’s look at some code examples demonstrating these implications.
String and &str usageThe examples below show some of the situations mentioned previously and contain suggestions on handling each of them.
Keep in mind that these are isolated, contrived examples and what you should do in your code will depend on many other factors. However, you can use these examples as a basic guideline.
This is the simplest example. If you need a constant string in your application, there’s one good way to implement it:
const CONST_STRING: &'static str = "some constant string";
CONST_STRING is read-only and will live statically inside your executable and be loaded to memory on execution.
If you have a String and you want to mutate it in a function, you can use &mut String as an argument:
fn main() {
let mut mutable_string = String::from("hello ");
do_some_mutation(&mut mutable_string);
println!("{}", mutable_string); // hello add this to the end
}
fn do_some_mutation(input: &mut String) {
input.push_str("add this to the end");
}
This way, you can mutate your actual String without having to allocate a new String. However, an allocation may take place in this case as well, for example, if the initial capacity of your String is too small to hold the new content.
There are cases where you need an owned string; for example, when returning a string from a function, or if you want to pass it to another thread with ownership (e.g., return a value, pass it to another thread, or building at runtime):
fn main() {
let s = "hello_world";
println!("{}", do_something(s)); // HELLO_WORLD
}
fn do_something(input: &str) -> String {
input.to_ascii_uppercase()
}
In addition, if we need to pass something with ownership, we have to pass in a String:
struct Owned {
bla: String,
}
fn create_owned(other_bla: String) -> Owned {
Owned { bla: other_bla }
}
Because Owned needs an owned string, if we pass in a &String, or &str, we would have to convert it to a String inside the function, triggering an allocation, so we might as well just pass in a String in this case.
If we don’t mutate the string, we can just use &str as the argument type. This will work even with String, because &String can be Deref coerced to &str:
const CONST_STRING: &'static str = "some constant string";
fn main() {
let s = "hello_world";
let mut mutable_string = String::from("hello");
print_something(&mutable_string);
print_something(s);
print_something(CONST_STRING);
}
fn print_something(something: &str) {
println!("{}", something);
}
As you can see, we can use &String, &'static str, and &str as input values for our print_something method.
We can use both String and &str with structs. The important difference is that if a struct needs to own its data, you need to use String. If you use &str, you need to use Rust lifetimes and make sure that the struct does not outlive the borrowed string, otherwise, it won’t compile.
For example, this won’t work:
struct Owned {
bla: String,
}
struct Borrowed<'a> {
bla: &'a str,
}
fn create_something() -> Borrowed { // error: nothing to borrow from
let o = Owned {
bla: String::from("bla"),
};
let b = Borrowed { bla: &o.bla };
b
}
With this code, the Rust compiler will generate an error indicating that Borrowed contains a borrowed value, but o, which it was borrowed from, goes out of scope. So when we return our Borrowed, the value we borrow from goes out of scope.
We can fix this by passing in the value to be borrowed:
struct Owned {
bla: String,
}
struct Borrowed<'a> {
bla: &'a str,
}
fn main() {
let o = Owned {
bla: String::from("bla"),
};
let b = create_something(&o.bla);
}
fn create_something(other_bla: &str) -> Borrowed {
let b = Borrowed { bla: other_bla };
b
}
This way, when we return Borrowed, the borrowed value is still in scope.
There are tons of built-in methods for performing string operations in Rust. We will explore some of them in this section.
You can use the string slice to reference a subset of characters in a string. You can create a string slice using a range of integers within square brackets with the [starting_index..ending_index] syntax. The slice will reference characters from starting_index up to but not including ending_index:
let hello_world = String::from("hello world");
let ello = &hello_world[1..5]; //ello
let orld = &hello_world[7..11]; //orld
You can omit starting_index if you want to reference the characters from index 0:
let hello = &hello_world[..5];
Similarly, you can also omit ending_index if you want to reference up to the last character:
let orld = &hello_world[7..];
You can omit both starting_index and ending_index to slice the entire string:
let hello_world_ref = &hello_world[..];
As hinted above, Rust stores strings as a sequence of UTF-8 encoded bytes. Therefore, the above examples work if the string only consists of single-byte characters. If your string has multi-byte characters, you must slice at character boundaries. Otherwise, Rust will panic if you slice in the middle of a multi-byte character.
As an example, the ❤️ emoji in the string "hello ❤️" is encoded using a sequence of six bytes. The bytes from index 6 to index 11 will encode the heart emoji. Therefore, your code will panic if the value of your starting_index or ending_index is between 6 and 12 because you’re slicing in the middle of a multi-byte character:
let hello_love = String::from("hello ❤️");
let heart = &hello_love[6..12];
let broken_heart = &hello_love[6..8]; //error
contains methodThe contains method, as its name suggests, is for checking the composition in a string slice. You can use it to check whether a string slice contains another string or sub-string. It returns a Boolean.
The contains method returns true if the pattern you pass as an argument matches a sub-slice of the string slice; otherwise, it returns false. The pattern you pass to the contains method can be a &str, char, or slice of chars. It can also be a function or closure that determines if a character matches:
let test_string = "hello ❤️";
assert!(test_string.contains("hell"));
assert!(test_string.contains("ell ❤️")); // This assertion will fail
starts_with methodIn Rust, you can use the starts_with method to determine if a string slice starts with another string or sub-string. It returns true if the string starts with the specified pattern; otherwise, it returns false:
let my_string: &str = "Hello world!";
assert!(my_string.starts_with("Hell"));
assert!(my_string.starts_with("hell")); // This assertion will fail
Be aware that starts_with is case-sensitive. Therefore, in the above example, it will consider the strings "Hell" and "hell" to be different prefixes.
find methodThe find method is for finding the first occurrence of a pattern in a string slice. It takes the pattern as an argument and returns the byte index of the first character of the string slice that matches the pattern:
let my_string = "Hello ❤️";
assert_eq!(my_string.find("❤️"), Some(6));
assert_eq!(my_string.find("k"), None);
assert_eq!(my_string.find("Hell"), Some(0));
assert_eq!(my_string.find("hell"), Some(0));
The find method returns None if there is no match for the pattern in the string slice. Be aware that the find method is case-sensitive. Hello ❤️ won’t match with hello ❤️.
rfind methodThe rfind method is similar to the find method explained above. Unlike find, rfind is for finding the last occurrence of a pattern in a string. The find method returns the byte index of the first character of the first match, while the rfind method returns the byte index of the first character of the last match:
let my_string = "Hello lovely";
assert_eq!(my_string.rfind("❤️"), None);
assert_eq!(my_string.rfind("el"), Some(9));
assert_eq!(my_string.rfind("hell"), None);
As an example, you may want to find the sub-string "el" in the string "Hello lovely". The sub-string "el" appears twice in the string "Hello lovely". The find method returns the byte index of the first e while the rfind method returns the byte index of the second e. The rfind method returns None if it doesn’t find a match.
In Rust, you can use the to_string method to convert any type that implements the ToString trait to a String:
let my_bool: bool = true;
let my_int: i32 = 23;
let my_float: f32 = 3.14;
let my_char: char = 'a';
println!("{}", my_bool.to_string());
println!("{}", my_int.to_string());
println!("{}", my_float.to_string());
println!("{}", my_char.to_string());
Any type that implements the Display trait also implements the ToString trait out of the box. Therefore, you don’t need to implement the ToString trait directly. You can implement the Display trait instead. The Rust documentation has a list of all the built-in types that implement the Display trait.
There are primarily two string types in Rust. Sometimes, you may want to convert one string type to another. As explained above, you can use the String::from method to convert a string slice to a String:
let my_string = String::from("Hello world");
Conversely, you can use the as_str method to convert a String to a string slice. The as_str method borrows the underlying data without copying:
let my_string = String::from("Hello world.");
let my_string_slice = my_string.as_str();
println!("{}", my_string);
println!("{}", my_string_slice);
You may want to convert a string into a number type or any other type that implements the FromStr trait. The parse method comes in handy for doing just that:
let my_string = "10";
let parsed_string: u32 = my_string.parse().unwrap();
println!("{parsed_string}");
Because you can parse a string into any type that implements the FromStr trait, you must specify an explicit type, like in the example above, in case you run into problems because of type inference. The Rust documentation has a list of built-in types that implement the FromStr trait. For more information about Rust traits, check out “Rust traits: A deep dive.”
Instead of using type annotation as above, you can also use Rust’s “turbofish” syntax like so:
let my_string = "10";
let parsed_string = my_string.parse::<u32>().unwrap();
println!("{parsed_string}");
If you’re using the parse method to convert strings to number types, make sure the string has valid characters. The string you’re parsing may have invalid characters such as units, leading and trailing white spaces, and locale-specific formatting, such as thousand separators.
You can use built-in methods such as trim, trim_start, trim_matches, and replace to remove invalid characters before parsing the string into a number if the string is ill-formatted as in the example below:
let price = " 200,000 ";
println!("{}", price.trim().replace(",", "").parse::<u32>().unwrap());
Similarly, the number you’re parsing may be out of range for the target numeric type. Be on the lookout for potential errors arising from overflows.
The parse method returns an error if it’s not possible to parse a string to the specified type. Be sure to handle errors appropriately.
For more complex parsing needs, you can also use the regex crate. It has capabilities that the built-in standard Rust modules may not have:
use regex::Regex;
fn main() {
let price = "$ 1,500";
let regx = Regex::new(r"[^0-9.]").unwrap();
println!("{}", regx.replace_all(price, ""));
}
In this article, we explored the difference between the Rust String and str string types, looking at how they each impact your Rust coding practices. We also presented several code examples to illustrate the practical use cases for each string type and the rationale for choosing them.
I hope this article was useful in dispelling some of the confusion you might have had about your string-related compilation errors in Rust, or if you’re more experienced with the language, to help you write more effective Rust code.
Debugging Rust applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking the performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.
LogRocket lets you replay user sessions, eliminating guesswork around why bugs happen by showing exactly what users experienced. It captures console logs, errors, network requests, and pixel-perfect DOM recordings — compatible with all frameworks.
LogRocket's Galileo AI watches sessions for you, instantly identifying and explaining user struggles with automated monitoring of your entire product experience.
Modernize how you debug your Rust apps — start monitoring for free.

:has(), with examplesThe CSS :has() pseudo-class is a powerful new feature that lets you style parents, siblings, and more – writing cleaner, more dynamic CSS with less JavaScript.

Kombai AI converts Figma designs into clean, responsive frontend code. It helps developers build production-ready UIs faster while keeping design accuracy and code quality intact.

Discover what’s new in The Replay, LogRocket’s newsletter for dev and engineering leaders, in the October 22nd issue.

John Reilly discusses how software development has been changed by the innovations of AI: both the positives and the negatives.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up now
2 Replies to "Understanding <code>String</code> and <code>&str</code> in Rust"
I’m currently learning rust and I think I’ve come back to this article 50 times now as I keep getting confused
Great article! I’ve been reading u for more than one year and I keep wondering how is it possible for one person to know so much. And many thanks for your articles how to use warp. It helped me a lot some time before.