Question
How can a Rust &str be converted to a &[u8]?
For example, this code does not compile because f expects a byte slice:
fn f(s: &[u8]) {}
fn main() {
let x = "a";
f(x);
}
The compiler reports that it expected &[u8] but found &str. Rust documentation says that str has a direct mapping to byte slices, so how should this conversion be performed?
Short Answer
You will learn that &str and &[u8] both refer to UTF-8 bytes but are intentionally different Rust types. You can borrow the underlying bytes with as_bytes() without allocating or copying data.
Concept
A Rust &str is a borrowed string slice containing valid UTF-8. A &[u8] is a borrowed slice of arbitrary bytes.
Although a &str is represented internally as a view over UTF-8 bytes, Rust does not automatically treat it as &[u8]. The types carry different guarantees:
&strguarantees that its contents are valid UTF-8.&[u8]makes no text-encoding guarantee; it may contain any byte values.
Use str::as_bytes to borrow a string's bytes:
fn f(bytes: &[u8]) {
println!("{bytes:?}");
}
fn main() {
let text = "hello";
f(text.as_bytes());
}
as_bytes() is a zero-copy conversion: the returned byte slice points at the same data as the original &str.
Mental Model
Think of a &str as a package of bytes with a UTF-8 verified label attached.
A &[u8] is the same kind of package after removing that label: you can inspect each byte, but Rust no longer promises that the bytes form readable UTF-8 text.
as_bytes() does not repack the package. It simply lets you view the same data as individual bytes.
Syntax and Examples
Use as_bytes() on a str or String reference.
fn print_bytes(bytes: &[u8]) {
println!("{bytes:?}");
}
fn main() {
let text: &str = "Rust";
let bytes: &[u8] = text.as_bytes();
print_bytes(bytes);
}
Output:
[82, 117, 115, 116]
For a String, use the same method:
fn main() {
let name = String::from("Ada");
let bytes: &[u8] = name.as_bytes();
assert_eq!(bytes, &[65, 100, 97]);
}
bytes borrows from name, so name must remain alive while bytes is used.
UTF-8 characters may use more than one byte:
let text = "é";
assert_eq!(text.len(), 2); // number of UTF-8 bytes
assert_eq!(text.as_bytes(), &[195, 169]);
assert_eq!(text.chars().count(), 1); // number of Unicode scalar values
Step by Step Execution
Consider this program:
fn send(bytes: &[u8]) {
println!("Sending {bytes:?}");
}
fn main() {
let message = "OK";
let raw_bytes = message.as_bytes();
send(raw_bytes);
}
Step by step:
messageis a&strreferring to the UTF-8 text"OK".message.as_bytes()creates a&[u8]view of that exact text data.- ASCII
Ohas byte value79, andKhas byte value75. raw_bytestherefore refers to[79, 75].sendreceives the byte slice and prints it.
No Vec<u8>, heap allocation, or byte-by-byte copy is created.
Real World Use Cases
&[u8] is useful whenever an API operates on raw bytes rather than Rust text.
- Writing to a network connection: protocols send bytes.
- Hashing and cryptography: hash functions commonly accept byte slices.
- Writing files:
std::fs::writecan write byte-oriented content. - Binary formats: headers, encoded messages, and checksums are byte data.
- FFI and system APIs: lower-level interfaces often receive buffers of bytes.
Example: hashing text with a function that accepts bytes:
fn simple_sum(bytes: &[u8]) -> u32 {
bytes.iter().map(|&byte| byte as u32).sum()
}
fn main() {
let input = "abc";
let result = simple_sum(input.as_bytes());
assert_eq!(result, 294);
}
Real Codebase Usage
In production Rust code, as_bytes() is typically used at a boundary where text becomes byte-oriented data.
Pass text to a byte API
fn write_packet(payload: &[u8]) {
// Send or store payload.
}
fn send_username(username: &str) {
write_packet(username.as_bytes());
}
Validate before parsing bytes
When data starts as bytes, validate it before treating it as text:
fn parse_header(bytes: &[u8]) -> Result<&str, std::str::Utf8Error> {
std::str::from_utf8(bytes)
}
This is the reverse direction. from_utf8 checks the UTF-8 invariant required by &str.
Avoid unnecessary allocations
Do not create a Vec<u8> merely to call an API that accepts &[u8]:
let text = "event";
process(text.as_bytes()); // preferred
Use text.as_bytes().to_vec() only when you specifically need an owned, mutable, or independently stored byte buffer.
Common Mistakes
Passing &str where &[u8] is required
This fails because Rust does not perform this conversion implicitly:
fn f(_: &[u8]) {}
let text = "a";
f(text); // error: expected &[u8], found &str
Fix it with as_bytes():
f(text.as_bytes());
Assuming one character always equals one byte
This works for ASCII but not all Unicode text:
let text = "é";
assert_eq!(text.chars().count(), 1);
assert_eq!(text.as_bytes().len(), 2);
Use .chars() when working with Unicode scalar values, and .as_bytes() when working with UTF-8 encoding bytes.
Using as for the conversion
A cast is not the correct operation:
let text = "hello";
// let bytes = text as &[u8]; // does not compile
Use the explicit and safe API: text.as_bytes().
Converting bytes back to text without validation
Arbitrary bytes may not be valid UTF-8:
let bytes = [0xFF];
let text = std::str::from_utf8(&bytes);
assert!(text.is_err());
Use std::str::from_utf8 and handle its Result when converting &[u8] to &str.
Comparisons
| Type or operation | Meaning | Allocation/copy | UTF-8 guarantee |
|---|---|---|---|
&str | Borrowed UTF-8 text | No | Yes |
&[u8] | Borrowed arbitrary bytes | No | No |
text.as_bytes() | Views &str as bytes | No | The resulting type does not express it |
std::str::from_utf8(bytes) | Attempts to view bytes as text | No | Checked at runtime |
String | Owned, growable UTF-8 text | May allocate when created | Yes |
Vec<u8> | Owned, growable byte buffer | May allocate when created | No |
The two main conversion directions are:
// &str -> &[u8]: always safe; UTF-8 is already known.
let bytes: &[u8] = "hello".as_bytes();
// &[u8] -> &str: must validate UTF-8.
let text: &str = std::str::from_utf8(b"hello")?;
Cheat Sheet
// Borrow UTF-8 text as bytes.
let text: &str = "hello";
let bytes: &[u8] = text.as_bytes();
// Works for String too.
let name = String::from("hello");
let bytes: &[u8] = name.as_bytes();
// Convert bytes to text after UTF-8 validation.
let text: Result<&str, std::str::Utf8Error> = std::str::from_utf8(bytes);
// Create owned bytes only when ownership or mutation is needed.
let owned: Vec<u8> = text.as_bytes().to_vec();
as_bytes()borrows; it does not copy.- Byte length is
text.len()ortext.as_bytes().len(). - Character count may differ from byte length for non-ASCII text.
- A
&[u8]does not guarantee valid UTF-8. - Use
String::from_utf8for an ownedVec<u8>that should become an ownedString.
FAQ
Is as_bytes() a copy in Rust?
No. It returns a borrowed &[u8] pointing to the same UTF-8 data as the original &str.
Why does Rust not automatically convert &str to &[u8]?
They have different meanings and guarantees. Rust requires an explicit conversion so code clearly shows when it starts handling text as raw bytes.
How do I convert &[u8] back to &str?
Use std::str::from_utf8(bytes). It returns a Result because the bytes may not be valid UTF-8.
Does &str always use UTF-8?
Yes. Rust string slices are always valid UTF-8.
Can I modify the bytes returned by as_bytes()?
No. as_bytes() returns an immutable &[u8]. Modifying bytes could break the UTF-8 validity required by str.
Should I use .len() to count characters in a Rust string?
Not usually. str::len() counts bytes. Use .chars().count() when you need a count of Unicode scalar values.
How do I get an owned Vec<u8> from a string?
Use text.as_bytes().to_vec(). This copies the bytes into a new vector.
Mini Project
Description
Build a small UTF-8 message inspector. It accepts text, passes it to a byte-oriented function, and reports both the byte values and the number of Unicode characters. This mirrors logging, protocol, and encoding-related utilities.
Goal
Convert a &str to &[u8] without copying and display the resulting UTF-8 bytes.
Requirements
Create a function that accepts a &[u8] and prints each byte.
Accept text as a &str in main.
Use as_bytes() to pass the text to the byte-processing function.
Display both the byte length and Unicode character count.
Use a non-ASCII example to show that bytes and characters can differ.
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.