Can you write this from memory?
Create a String variable `name` with the value "Alice".
Rust has two main string types: String (owned, heap-allocated, growable) and &str (borrowed slice, usually a view into a String or a string literal).
The distinction trips up every Rust learner. When do you use String? When &str? How do you convert between them? Why can't you index into a string?
This page focuses on the string operations you'll use constantly, from conversions to safe slicing to the UTF-8 gotchas that catch everyone.
String | &str | |
|---|---|---|
| Ownership | Owned, heap-allocated | Borrowed slice (view into data) |
| Mutability | Growable with push_str, push | Immutable |
| Use for | Storing, building, returning | Function parameters, string literals |
| Size | 3 words (ptr + len + capacity) | 2 words (ptr + len) |
| Literal type | — | "hello" is &'static str |
Rule of thumb: take &str for input parameters (accepts both String and literals via deref coercion). Use String when you need ownership or mutation. For a copy-ready reference of all string types and conversions, see the Rust string types cheat sheet.
The most common string error. A struct or function wants an owned String but you passed a literal:
struct Config {
name: String,
}
// WRONG: "app" is &str, not String
// let c = Config { name: "app" }; // error[E0308]
// RIGHT: convert to String
let c = Config { name: String::from("app") };
// or
let c = Config { name: "app".to_string() };
// or
let c = Config { name: "app".to_owned() };
Rust strings are UTF-8 encoded. A single character can be 1--4 bytes, so s[0] would be ambiguous (byte? character? grapheme?):
// WRONG: direct indexing
// let s = String::from("hello");
// let c = s[0]; // error[E0277]: String cannot be indexed by {integer}
// RIGHT: use .chars() for character access
let s = String::from("hello");
let c = s.chars().nth(0); // Some('h')
// RIGHT: byte slicing when you know the boundaries
let first_byte = &s[0..1]; // "h" — safe for ASCII
An &str slice borrows the String. The ownership and borrowing rules apply directly here: you cannot mutate the String while the slice is alive:
let mut s = String::from("hello world");
// WRONG: slice borrows s, then we try to mutate
// let first = &s[..5];
// s.push_str("!"); // error[E0502]
// println!("{first}");
// RIGHT: scope the slice, or copy what you need
let first = s[..5].to_string(); // copy into owned String
s.push_str("!"); // no active borrow
println!("{first}");
&s[a..b] panics if the range lands inside a multi-byte character. Use get() for non-panicking slicing:
let s = "cafe\u{0301}"; // "café" — e + combining accent
// PANICS: byte 5 is inside the combining accent
// let bad = &s[0..5];
// SAFE: get() returns None on invalid boundaries
let safe = s.get(0..4); // Some("cafe")
let invalid = s.get(0..5); // None (not a char boundary)
// Check boundaries explicitly
if s.is_char_boundary(4) {
let slice = &s[0..4]; // "cafe"
}
Make get() your default for any range that comes from user input or computation.
.len() counts bytes, not characters. These three measurements can all differ:
let s = "cafe\u{0301}"; // "café" (e + combining accent)
s.len() // 6 bytes (UTF-8 encoded)
s.chars().count() // 5 Unicode scalar values (c, a, f, e, combining accent)
// grapheme clusters: 4 ("c", "a", "f", "é") — need unicode-segmentation crate
| Method | Counts | Use for |
|---|---|---|
.len() | Bytes | Buffer sizes, capacity, I/O |
.chars().count() | Unicode scalar values | Character-level processing |
| Graphemes (external crate) | User-perceived characters | Display width, text editing |
For grapheme clusters, use the unicode-segmentation crate: UnicodeSegmentation::graphemes(s, true).count().
// &str → String (three equivalent ways)
let s1 = "hello".to_string();
let s2 = String::from("hello");
let s3 = "hello".to_owned();
// String → &str (deref coercion or explicit)
let owned = String::from("hello");
let borrowed: &str = &owned; // deref coercion
let also_borrowed = owned.as_str(); // explicit
// Takes &str: accepts both String and &str via deref coercion
fn greet(name: &str) {
println!("Hello, {name}!");
}
let owned = String::from("world");
greet(&owned); // String auto-derefs to &str
greet("world"); // &str passed directly
let mut s = String::new();
s.push_str("hello");
s.push(' ');
s.push_str("world");
// format! for complex concatenation (most readable)
let combined = format!("{} {} {}", year, month, day);
For hot loops, pre-reserve capacity and use push_str. If you're building strings from collection data, the iterators guide shows how to chain .map() and .collect::<String>() for the same effect:
let mut buf = String::with_capacity(1024);
for line in lines {
buf.push_str(line);
buf.push('\n');
}
For compile-time literal concatenation, use concat!:
const GREETING: &str = concat!("hello", " ", "world"); // &'static str
- The Rust Book: Strings — String vs &str, UTF-8, slicing
- Rust By Example: Strings — practical string patterns
- std::string::String — full method reference
- unicode-segmentation crate — grapheme cluster iteration
When to Use Rust Strings: String vs &str, Ownership & Methods
- Use `&str` for function parameters when you only need to read the string.
- Use `String` when you need ownership or need to modify the string.
- Use `format!()` for complex string concatenation.
- Use `.push_str()` to append to a String efficiently.
- Use `.chars()` to iterate over Unicode characters.
Check Your Understanding: Rust Strings: String vs &str, Ownership & Methods
What is the difference between String and &str in Rust?
String is an owned, heap-allocated, growable UTF-8 string. &str is a borrowed slice (a view) into string data. String literals are &str. You can borrow a String as &str, but converting &str to String requires allocation.
What You'll Practice: Rust Strings: String vs &str, Ownership & Methods
Common Rust Strings: String vs &str, Ownership & Methods Pitfalls
- `.len()` returns a smaller number than expected -- `.len()` counts bytes, not characters. A multi-byte emoji like "\u{1F980}" is 4 bytes. Use `.chars().count()` for character count.
- `s[0]` won't compile -- Rust strings are UTF-8 and indexing by integer is not allowed because byte position may not align with character boundaries. Use `.chars().nth(0)` for the first character.
- Program panics on `&s[0..n]` -- slicing at a byte offset inside a multi-byte character panics. Use `s.get(0..n)` which returns `Option<&str>`, or check `.is_char_boundary(n)` first.
- String grows slowly in a loop using `+` -- each `+` allocates a new String and moves the left operand. Use `push_str()` on a single `mut String`, ideally with `String::with_capacity()` pre-allocated.
- `.chars().count()` gives wrong count for emoji/accented text -- `.chars()` counts Unicode scalar values, not user-perceived characters. The string "cafe\u{0301}" has 5 chars but 4 visible characters. For grapheme clusters, use the `unicode-segmentation` crate.
- Function rejects your `&String` argument -- deref coercion usually converts `&String` to `&str` automatically, but trait bounds or generics can block it. Call `.as_str()` explicitly when coercion fails.
Rust Strings: String vs &str, Ownership & Methods FAQ
Why can't I index a string with s[0]?
Rust strings are UTF-8, where characters can be 1-4 bytes. Indexing by byte position could slice in the middle of a character. Use `.chars().nth(0)` for the first character or byte slicing `&s[0..1]` if you know the byte boundaries.
When should I use String vs &str in function parameters?
Prefer `&str` for input parameters—it accepts both String (via deref coercion) and &str. Use `String` when the function needs to take ownership or return an owned string.
How do I concatenate strings?
Use `format!()` for readability, `push_str()` for efficiency when building incrementally, or `+` operator (which consumes the left String). Avoid repeated `+` in loops—use `push_str()` instead.
What does .len() return for strings?
`.len()` returns the number of bytes, not characters. For character count, use `.chars().count()`. For "🦀".len() you get 4 (bytes), but .chars().count() gives 1.
How do I convert between String and &str?
&str to String: `.to_string()`, `String::from()`, or `.to_owned()`. String to &str: `&s`, `s.as_str()`, or automatic deref coercion in function calls.
How do I slice a string safely without panicking?
Use `s.get(start..end)` which returns `Option<&str>` instead of panicking. Or check `s.is_char_boundary(n)` before using `&s[..n]`. Never use raw byte slicing on untrusted input.
What is the difference between chars() and graphemes?
`.chars()` yields Unicode scalar values. Grapheme clusters are user-perceived characters (e.g., "e" + combining accent = one grapheme but two chars). For graphemes, use the `unicode-segmentation` crate.
Rust Strings: String vs &str, Ownership & Methods Syntax Quick Reference
let s: &str = "hello";let s = String::from("hello");let s = "hello".to_string();let slice: &str = &my_string;s.push_str("world");s.push('!');let s = format!("{} {}", a, b);for c in s.chars() {
println!("{c}");
}let slice = &s[0..5];let safe = s.get(0..5); // Option<&str>let count = s.chars().count();let mut s = String::with_capacity(256);const S: &str = concat!("hello", " ", "world");Rust Strings: String vs &str, Ownership & Methods Sample Exercises
Fill in the method to convert a &str to a String.
to_stringFill in the method that creates an owned String from a borrowed &str.
to_ownedFill in the type of a string literal.
&str+ 56 more exercises
Copy-ready syntax examples for quick lookup