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:
strandStringrepresent text[u8]andVec<u8>represent bytes- Borrowed types (
&str,&[u8]) do not own memory - Owned types (
String,Vec<u8>) own memory and may allocate
Why this matters
In real Rust code:
- APIs that process text often take
&str - APIs that store or build text often use
String - File, network, and binary APIs often use
&[u8]orVec<u8> - Converting bytes to text may fail if the bytes are not valid UTF-8
This is why some conversions are simple borrows, some require cloning or allocation, and some return Result.
A useful rule
Ask two questions:
- Am I switching between borrowed and owned data?
- If yes, you may need to clone or move ownership.
- Am I switching between bytes and text?
- If yes, UTF-8 validity matters.
Complete conversion table
| From | To | Idiomatic conversion | Allocates? | Can fail? |
|---|---|---|---|---|
&str | String | st.to_string() or String::from(st) | Yes | No |
&str | &[u8] | st.as_bytes() | No | No |
&str | Vec<u8> | st.as_bytes().to_vec() | Yes | No |
String | &str | s.as_str() or &s | No | No |
String | &[u8] | s.as_bytes() | No | No |
String | Vec<u8> | s.into_bytes() | No extra copy | No |
&[u8] | &str | std::str::from_utf8(u) | No | Yes |
&[u8] | String | String::from_utf8(u.to_vec()) | Yes | Yes |
&[u8] | Vec<u8> | u.to_vec() | Yes | No |
Vec<u8> | &str | std::str::from_utf8(&v) | No | Yes |
Vec<u8> | String | String::from_utf8(v) | No extra copy | Yes |
Vec<u8> | &[u8] | &v or v.as_slice() | No | No |
Important UTF-8 note
Rust strings are always valid UTF-8. That means:
&str -> &[u8]is always safeString -> Vec<u8>is always safe&[u8] -> &strmay failVec<u8> -> Stringmay fail
That single UTF-8 rule explains most of the API design around these types.
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
Vertical moves
&str -> &[u8]: text viewed as UTF-8 bytesString -> Vec<u8>: owned text turned into owned bytes&[u8] -> &str: bytes interpreted as text, only if valid UTF-8Vec<u8> -> String: owned bytes interpreted as text, only if valid UTF-8
Simple analogy
Stringis like a book you own&stris like reading a page from that bookVec<u8>is like a box of raw paper scraps you own&[u8]is like looking at some scraps without owning them
You can always look at text as bytes, because text is stored as bytes. But you cannot always treat bytes as text, because not every byte sequence is valid UTF-8 text.
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();
No allocation happens here. You are just viewing the string as bytes.
Borrowed text to owned bytes
let bytes_vec: Vec<u8> = st.as_bytes().to_vec();
This creates a new vector containing a copy of the bytes.
Owned text to borrowed text
let text: &str = s.as_str();
// or simply:
let text2: &str = &s;
Both are idiomatic.
Owned text to borrowed bytes
let bytes: &[u8] = s.as_bytes();
No copy. You borrow the bytes inside the String.
Owned text to owned bytes
let s = String::from("hello");
let bytes: Vec<u8> = s.into_bytes();
into_bytes() consumes the String and reuses its buffer.
Borrowed bytes to borrowed text
let u: &[u8] = b"hello";
let text: Result<&str, std::str::Utf8Error> = std::str::from_utf8(u);
This returns Result because bytes may not be valid UTF-8.
Borrowed bytes to owned text
let u: &[u8] = b"hello";
let text: Result<String, std::string::FromUtf8Error> = String::from_utf8(u.to_vec());
You first create an owned Vec<u8>, then try to convert it into String.
A common alternative is:
let text: Result<String, std::str::Utf8Error> =
std::str::from_utf8(u).map(str::to_owned);
Borrowed bytes to owned bytes
let copy: Vec<u8> = u.to_vec();
Owned bytes to borrowed text
let v: Vec<u8> = b"hello".to_vec();
let text: Result<&str, std::str::Utf8Error> = std::str::from_utf8(&v);
Owned bytes to owned text
let v: Vec<u8> = b"hello".to_vec();
let text: Result<String, std::string::FromUtf8Error> = String::from_utf8(v);
This consumes the vector.
Owned bytes to borrowed bytes
let slice: &[u8] = &v;
// or
let slice2: &[u8] = v.as_slice();
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.as_bytes();
bytesis a&[u8]- No new memory is allocated
bytespoints into the same data owned bys
At this point, the bytes are roughly:
[104, 105]
because h = 104 and i = 105
3. Copy the bytes into a new vector
let copied = bytes.to_vec();
copiedis a newVec<u8>- This allocates memory and copies the bytes
- Now
copiedowns its own byte buffer
4. Try to interpret the bytes as UTF-8 text
let text = std::str::from_utf8(&copied).unwrap();
&copiedbecomes a&[u8]from_utf8checks whether those bytes are valid UTF-8- Since
[104, 105]is valid UTF-8, it returnsOk("hi") unwrap()extracts the&str
5. Print the result
println!("{}", text);
Output:
hi
What to notice
as_bytes()borrowsto_vec()copiesfrom_utf8()validates- Borrowed forms are cheap
- Owned forms may allocate or move data
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:
fn is_yes(input: &str) -> bool {
input.trim() == "yes"
}
&str is ideal when you only need to read text.
Storing generated content
If your program builds text dynamically, use String:
let mut report = String::new();
report.push_str("Status: ");
report.push_str("ready");
Processing binary formats
Image formats, compressed data, protobuf messages, and encrypted payloads are usually handled as Vec<u8> or &[u8], not strings.
Converting API payloads
JSON and text-based protocols often arrive as bytes and must be validated as UTF-8 before parsing:
fn parse_json_bytes(data: &[u8]) {
let text = std::str::from_utf8(data).expect("expected UTF-8 JSON");
println!("JSON text: {}", text);
}
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
Store text in structs when it must outlive the current scope:
struct User {
name: String,
}
Guard clauses for UTF-8 validation
fn parse_text(data: &[u8]) -> Result<&str, std::str::Utf8Error> {
let text = std::str::from_utf8(data)?;
Ok(text)
}
This is a common pattern: validate early, then work with &str.
Avoid unnecessary allocation
A common pattern is to borrow first and only allocate if needed:
fn print_uppercase(data: &[u8]) {
if let Ok(text) = std::str::from_utf8(data) {
println!("{}", text.to_uppercase());
}
}
The borrowed &str avoids making a String until to_uppercase() creates one.
Convert owned text into owned bytes efficiently
fn send_payload(text: String) -> Vec<u8> {
text.into_bytes()
}
into_bytes() is preferred when you already own the String.
Common pattern: AsRef<[u8]> and AsRef<str>
Many libraries accept generic borrowed input:
fn checksum<T: AsRef<[u8]>>(data: T) -> usize {
data.as_ref().len()
}
This allows callers to pass &[u8], Vec<u8>, String, and &str in some contexts.
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 print_name(name: &str) {
println!("{name}");
}
3. Using to_vec() when into_bytes() is better
Less efficient:
let s = String::from("hello");
let v = s.as_bytes().to_vec();
This copies bytes.
Better when you own s:
let s = String::from("hello");
let v = s.into_bytes();
4. Trying to return &str from temporary data
Broken code:
fn make_text() -> &str {
let s = String::from("hello");
s.as_str()
}
This does not compile because s is dropped at the end of the function.
Better:
fn make_text() -> String {
String::from("hello")
}
5. Forgetting that String::from_utf8 consumes the vector
let v = b"hello".to_vec();
let s = String::from_utf8(v);
After this, v has been moved and cannot be used again unless you clone it beforehand.
6. Confusing &String with &str
&String works in some places, but APIs usually prefer &str because it is more general.
Better function signature:
fn process(text: &str) {
println!("{text}");
}
7. Ignoring lossy conversions when appropriate
Sometimes invalid UTF-8 should not cause failure.
let bytes = b"hello\xFF";
let text = String::from_utf8_lossy(bytes);
println!("{text}");
This replaces invalid bytes with the Unicode replacement character.
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 | Represents text? | Represents arbitrary bytes? | UTF-8 guaranteed? |
|---|---|---|---|
&str | Yes | No | Yes |
String | Yes | No | Yes |
&[u8] | Sometimes | Yes | No |
Vec<u8> | Sometimes | Yes | No |
Similar conversions compared
| Conversion | Best choice | Why |
|---|---|---|
&str -> String | to_string() or String::from(...) | Clear and idiomatic |
String -> &str | &s or s.as_str() | Borrow without allocation |
String -> Vec<u8> | into_bytes() | Reuses owned buffer |
Vec<u8> -> String | String::from_utf8(v) | Validates UTF-8 and reuses buffer |
&[u8] -> &str | std::str::from_utf8(u) | Borrow plus validation |
&[u8] -> String | std::str::from_utf8(u)?.to_owned() | Clear when starting from borrowed bytes |
from_utf8 vs from_utf8_lossy
| Method | Return type | Fails on invalid UTF-8? | Use when |
|---|---|---|---|
std::str::from_utf8 | Result<&str, Utf8Error> | Yes | You need valid UTF-8 |
String::from_utf8 | Result<String, FromUtf8Error> | Yes | You own the bytes |
String::from_utf8_lossy | Cow<str> | No | You want readable text even if bytes are invalid |
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().to_vec();
// String -> &str
let d: &str = s.as_str();
let d2: &str = &s;
// String -> &[u8]
let e: &[u8] = s.as_bytes();
// String -> Vec<u8>
let f: Vec<u8> = s.into_bytes();
// &[u8] -> &str
let g = std::str::from_utf8(u); // Result<&str, Utf8Error>
// &[u8] -> String
let h = std::str::from_utf8(u).map(str::to_owned);
// or
let h2 = String::from_utf8(u.to_vec());
// &[u8] -> Vec<u8>
let i: Vec<u8> = u.to_vec();
// Vec<u8> -> &str
let j = std::str::from_utf8(&v); // Result<&str, Utf8Error>
// Vec<u8> -> String
let k = String::from_utf8(v); // Result<String, FromUtf8Error>
// Vec<u8> -> &[u8]
let l: &[u8] = &v;
let l2: &[u8] = v.as_slice();
Rules to remember
Stringand&strare always valid UTF-8Vec<u8>and&[u8]may contain any bytes- Borrowing is cheap
- Cloning or
to_vec()allocates into_*methods usually consume and reuse owned data- Byte-to-text conversions often return
Result
Best default choices
- Need read-only text input: use
&str - Need owned text: use
String - Need read-only bytes: use
&[u8] - Need owned bytes: use
Vec<u8>
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. &String is a reference to a String, while &str is a string slice. Many APIs prefer &str because it works with more kinds of string data.
What if my bytes are not valid UTF-8 but I still want a string?
Use String::from_utf8_lossy(bytes). Invalid byte sequences are replaced with the replacement character.
Does as_bytes() copy the data?
No. It returns a borrowed byte slice referencing the existing string data.
How do I choose between to_vec() and into_bytes()?
Use to_vec() when you only have a borrow and need an owned byte vector. Use into_bytes() when you already own the String and want to avoid copying.
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
Absolute Values in Rust: Using the abs() Method
Learn how to calculate absolute values in modern Rust with abs(), handle signed integer overflow, and choose safer alternatives.
Accepting Iterators in Rust Functions with impl Iterator
Learn how to write Rust functions that accept HashMap iterators using generic bounds or impl Iterator, avoiding trait object and lifetime errors.
Access Enum Variant Values in Rust with Pattern Matching
Learn how to extract and print values stored in Rust enum variants using match and if let pattern matching, with practical examples.