How to convert a Vec of bytes to a String
Short answer: String::from_utf8(bytes) checks the bytes and returns Result<String, FromUtf8Error>; on failure into_bytes() gives the Vec back. String::from_utf8_lossy(&bytes) cannot fail — anything that isn’t valid UTF-8 becomes U+FFFD, the replacement character. Use from_utf8 when the bytes are supposed to be text, from_utf8_lossy when you want readable output no matter what.
From a fresh project — cargo new utf8-demo — replace src/main.rs with each block below and run it with cargo run. The project name only shows up in cargo’s own output; nothing else on this page depends on it.
The checked conversion
Section titled “The checked conversion”String::from_utf8 takes the Vec<u8> by value and gives back a Result. These bytes spell “café” — five bytes, because é takes two:
fn main() { let bytes = vec![0x63, 0x61, 0x66, 0xC3, 0xA9]; // "café" in UTF-8 let text = String::from_utf8(bytes).unwrap(); println!("{text} is {} bytes", text.len());}café is 5 bytesThat check is what keeps the String guarantee real: a String can only hold valid UTF-8, and this is the function that enforces it at the boundary. On bytes you produced yourself, unwrap() is fine — a panic there means your own encoder is broken.
When the bytes aren’t UTF-8
Section titled “When the bytes aren’t UTF-8”Two things go wrong, and the message says which. A byte that can’t appear anywhere gives invalid utf-8 sequence; input that ends in the middle of a multi-byte character gives incomplete utf-8 byte sequence:
fn main() { let bytes = vec![0x68, 0x69, 0xC3]; match String::from_utf8(bytes) { Ok(text) => println!("{text}"), Err(err) => { println!("{err}"); let error = err.utf8_error(); println!("valid up to byte {}, error_len {:?}", error.valid_up_to(), error.error_len()); let bytes = err.into_bytes(); println!("{:?}", bytes); } }}incomplete utf-8 byte sequence from index 2valid up to byte 2, error_len None[104, 105, 195]0xC3 promises a two-byte character and the input ends first. error_len() is None there — more bytes might have finished the sequence — and Some(n) for a byte that’s wrong wherever it appears. into_bytes() hands the Vec back untouched, so a failed conversion costs nothing: retry with more data, log it, or fall back to the lossy path.
The lossy conversion
Section titled “The lossy conversion”String::from_utf8_lossy takes &[u8], returns a Cow<str>, and cannot fail:
use std::borrow::Cow;
fn main() { let broken = vec![0x68, 0x69, 0xC3]; let text = String::from_utf8_lossy(&broken); println!("{text} is {} chars", text.chars().count());
let latin1 = vec![0x63, 0x61, 0x66, 0xE9]; let text = String::from_utf8_lossy(&latin1); println!("{text}");
let good = vec![0x68, 0x69]; let text = String::from_utf8_lossy(&good); println!("borrowed: {}", matches!(text, Cow::Borrowed(_)));}hi� is 3 charscaf�borrowed: trueEach byte that can’t be part of a sequence becomes one U+FFFD; a sequence cut off by the end of the input becomes one for the whole partial sequence. The replacement character is three bytes, so repaired text can outgrow its input — three bytes in, five out on the first line. Valid input is borrowed, not copied: the last line proves no allocation happened.
That silence is the trade. from_utf8 refuses [0x63, 0x61, 0x66, 0xE9] and says where; from_utf8_lossy prints caf� and moves on. Two different broken inputs can print the same thing, so lossy output is for eyes, not for data you’ll parse again.
When you don’t need an owned String
Section titled “When you don’t need an owned String”str::from_utf8 borrows instead:
fn main() { let bytes = vec![0x68, 0x69]; let text = str::from_utf8(&bytes).unwrap(); println!("{text}"); println!("{:?}", bytes);}hi[104, 105]Same check, same Utf8Error on failure — the one FromUtf8Error carries, reachable through utf8_error() — but the Vec stays yours.
Related
Section titled “Related”- How to handle UTF-8 text — what the guarantee buys, and where it costs
- What is the difference between String and &str? — the buffer underneath
- How to reverse a string — the compiler refusing bytes that were never text
- Working with text — the rest of the chapter