Skip to content
Select theme

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.

String::from_utf8 takes the Vec<u8> by value and gives back a Result. These bytes spell “café” — five bytes, because é takes two:

café is 5 bytes

That 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.

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:

incomplete utf-8 byte sequence from index 2
valid 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.

String::from_utf8_lossy takes &[u8], returns a Cow<str>, and cannot fail:

hi� is 3 chars
caf�
borrowed: true

Each 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.

str::from_utf8 borrows instead:

hi
[104, 105]

Same check, same Utf8Error on failure — the one FromUtf8Error carries, reachable through utf8_error() — but the Vec stays yours.