Question
I am new to Rust and find it difficult to work comfortably with these related types: String, &str, Vec<u8>, and &[u8].
I want to understand the idiomatic ways to convert between them and, more importantly, when each conversion is appropriate.
Given these values:
let st: &str = "hello";
let s: String = String::from("hello");
let u: &[u8] = b"hello";
let v: Vec<u8> = b"hello".to_vec();
I believe the following conversions are correct, but I am not sure whether they are the most idiomatic choices:
// &str -> String
String::from(st)
// &str -> &[u8]
st.as_bytes()
// String -> &str
s.as_str()
// &[u8] -> &str
std::str::from_utf8(u)
// Vec<u8> -> String
String::from_utf8(v)
I would like a complete table of idiomatic transitions for all of these cases:
&str -> String&str -> &[u8]&str -> Vec<u8>String -> &strString -> &[u8]String -> Vec<u8>&[u8] -> &str&[u8] -> String&[u8] -> Vec<u8>Vec<u8> -> &strVec<u8> -> StringVec<u8> -> &[u8]
Short Answer
By the end of this page, you will understand how Rust converts between text types and byte types, when a conversion is free versus when it allocates, and which conversions can fail because of UTF-8 validation. You will also see the idiomatic methods for each direction and learn how ownership and borrowing affect your choices.
Concept
Rust separates text from raw bytes, and it also separates owned data from borrowed data.
These four types sit at the intersection of those two ideas:
| Type | Meaning | Owns data? | Must be valid UTF-8? |
|---|---|---|---|
&str | Borrowed string slice | No | Yes |
String | Owned growable string | Yes | Yes |
&[u8] | Borrowed byte slice | No | No |
Vec<u8> | Owned growable byte buffer | Yes | No |
The key idea is:
Mental Model
Think of these types as two axes:
- Text vs bytes
- Borrowed vs owned
Imagine a 2×2 grid:
| Borrowed | Owned | |
|---|---|---|
| Text | &str | String |
| Bytes | &[u8] | Vec<u8> |
Now think of conversions as moves on this grid:
- Moving left/right changes ownership
- Moving up/down changes representation
Horizontal moves
&str -> String: make an owned copyString -> &str: borrow from owned text&[u8] -> Vec<u8>: copy bytes into owned storageVec<u8> -> &[u8]: borrow bytes from the vector
Syntax and Examples
Core syntax
let st: &str = "hello";
let s: String = String::from("hello");
let u: &[u8] = b"hello";
let v: Vec<u8> = b"hello".to_vec();
Borrowed text to owned text
let owned: String = st.to_string();
let owned2: String = String::from(st);
Both are idiomatic. to_string() is often the most readable for beginners.
Borrowed text to borrowed bytes
let bytes: &[u8] = st.as_bytes();
Step by Step Execution
Consider this example:
fn main() {
let s = String::from("hi");
let bytes = s.as_bytes();
let copied = bytes.to_vec();
let text = std::str::from_utf8(&copied).unwrap();
println!("{}", text);
}
Step by step
1. Create an owned string
let s = String::from("hi");
sowns the text"hi"- Internally, that text is stored as UTF-8 bytes
2. Borrow its bytes
let bytes = s.();
Real World Use Cases
Working with files and network data
File and socket APIs commonly give you bytes:
fn handle_packet(data: &[u8]) {
if let Ok(text) = std::str::from_utf8(data) {
println!("Text message: {}", text);
} else {
println!("Binary message: {:?}", data);
}
}
Use &[u8] when the data may be binary or when you want to avoid assuming UTF-8.
Building HTTP responses
Web frameworks often accept text as String or &str, but lower-level protocols may need bytes:
let body = String::from("OK");
let raw: Vec<u8> = body.into_bytes();
Parsing user input
CLI tools often read text and borrow it as &str for parsing:
Real Codebase Usage
In real Rust codebases, developers usually choose the most flexible input type and the most useful ownership model.
Accept borrowed data in function parameters
Functions often accept &str or &[u8] instead of owned types:
fn log_message(msg: &str) {
println!("{msg}");
}
fn write_bytes(buf: &[u8]) {
println!("{} bytes", buf.len());
}
This lets callers pass either borrowed or owned values.
Use &str for read-only text APIs
fn greet(name: &str) -> String {
format!("Hello, {name}!")
}
Callers can pass:
- string literals
&String- slices of strings
Use String when ownership is needed
Common Mistakes
1. Assuming all byte slices are valid text
Broken code:
let bytes = &[0xff, 0xfe, 0xfd];
let text = std::str::from_utf8(bytes).unwrap();
This may panic because the bytes are not valid UTF-8.
Better:
match std::str::from_utf8(bytes) {
Ok(text) => println!("{text}"),
Err(err) => println!("Invalid UTF-8: {err}"),
}
2. Cloning when borrowing would be enough
Broken idea:
fn print_name(name: String) {
println!("{name}");
}
This forces ownership unnecessarily.
Better:
fn (name: &) {
();
}
Comparisons
Borrowed vs owned
| Type | Borrowed or owned? | Mutable size? | Typical use |
|---|---|---|---|
&str | Borrowed | No | Read-only text input |
String | Owned | Yes | Store or build text |
&[u8] | Borrowed | No | Read-only bytes |
Vec<u8> | Owned | Yes | Store or build byte data |
Text vs bytes
| Type |
|---|
Cheat Sheet
Quick reference
// Starting values
let st: &str = "hello";
let s: String = String::from("hello");
let u: &[u8] = b"hello";
let v: Vec<u8> = b"hello".to_vec();
Conversion table
// &str -> String
let a: String = st.to_string();
let a2: String = String::from(st);
// &str -> &[u8]
let b: &[u8] = st.as_bytes();
// &str -> Vec<u8>
let c: Vec<u8> = st.as_bytes().();
: & = s.();
: & = &s;
: &[] = s.();
: <> = s.();
= std::::(u);
= std::::(u).(::to_owned);
= ::(u.());
: <> = u.();
= std::::(&v);
= ::(v);
: &[] = &v;
: &[] = v.();
FAQ
When should I use &str instead of String in Rust?
Use &str when your function only needs to read text and does not need ownership. It is more flexible and avoids unnecessary allocation.
Why does &[u8] to &str return a Result?
Because not all byte slices contain valid UTF-8. Rust strings must always be valid UTF-8, so validation is required.
What is the most idiomatic way to convert String to Vec<u8>?
Use into_bytes() if you own the String. It consumes the string and reuses its buffer efficiently.
How do I convert Vec<u8> to String in Rust?
Use String::from_utf8(v). It checks UTF-8 validity and returns Result<String, FromUtf8Error>.
Is &String the same as &str?
Not exactly. is a reference to a , while is a string slice. Many APIs prefer because it works with more kinds of string data.
Mini Project
Description
Build a small Rust utility that accepts incoming byte data, tries to interpret it as UTF-8 text, and then prints both the text form and the raw bytes form. This demonstrates the most common conversions between borrowed and owned text/byte types in a realistic workflow similar to reading data from a file, socket, or API response.
Goal
Create a program that starts with byte data, validates it as UTF-8, converts it into both borrowed and owned text, and also shows how to convert text back into bytes.
Requirements
- Start with a
Vec<u8>containing UTF-8 text bytes. - Borrow the bytes as
&[u8]and attempt to convert them to&str. - Create an owned
Stringfrom the bytes. - Convert the
Stringback intoVec<u8>. - Print each step so the data flow is visible.
Keep learning
Related questions
Accessing Cargo Package Metadata in Rust
Learn how to read Cargo package metadata like version, name, and authors in Rust using compile-time environment macros.
Associated Types vs Generic Type Parameters in Rust: When to Use Each
Learn when to use associated types vs generic parameters in Rust traits, with clear rules, examples, and practical API design advice.
Can a Struct Extend Another Struct in Rust? Composition vs Inheritance
Learn how Rust handles struct reuse without inheritance, using composition, traits, and wrapper structs with practical examples.