How to read a file into a String
Short answer: std::fs::read_to_string(path) reads a whole file and hands back an io::Result<String>. It assumes the file is UTF-8 — bytes that aren’t valid UTF-8 come back as an Err with kind InvalidData, never as garbled text.
From a fresh project — cargo new read-demo, then replace the contents of src/main.rs with each block below and run it with cargo run. The project name shows up only in cargo’s own output; nothing else on this page depends on it. Each block writes the file it reads before reading it, so it runs on its own; in your own code the file is already there and you delete that line.
Read the whole file
Section titled “Read the whole file”use std::fs;use std::io;
fn main() -> io::Result<()> { fs::write("poem.txt", "Leaves are falling\nsoftly on the path\n")?; let text = fs::read_to_string("poem.txt")?; println!("{text}"); println!("{} bytes", text.len()); Ok(())}Leaves are fallingsoftly on the path
38 bytesOne call opens the file, reads it to the end and gives you a String — no buffer to size, no loop to write. The ? on each line is what turns a failure into an early return: main’s return type is io::Result<()>, so an error prints the message and exits with status 1 instead of panicking. The path is relative to the directory you run the program from, which under cargo run is the project root — that’s where poem.txt lands.
When the file isn’t there
Section titled “When the file isn’t there”A missing file is an error like any other, and it’s the one every reader hits first:
use std::fs;use std::io::ErrorKind;
fn main() { match fs::read_to_string("no-such-file.txt") { Ok(text) => println!("read {} bytes", text.len()), Err(err) => { println!("{err}"); println!("{err:?}"); println!("NotFound: {}", err.kind() == ErrorKind::NotFound); } }}No such file or directory (os error 2)Os { code: 2, kind: NotFound, message: "No such file or directory" }NotFound: trueThe two print lines are the same error in two forms: {err} is the human-readable Display, and {err:?} is the Debug, which also names the ErrorKind. Match on err.kind() when a missing file is an acceptable outcome — a config file that doesn’t exist yet is not a crash — and propagate everything else with ?.
The encoding assumption
Section titled “The encoding assumption”read_to_string returns a String, and a String can only hold valid UTF-8. So the function reads the bytes and then checks them, and a file in another encoding fails:
use std::fs;
fn main() { // Latin-1 for "café\n": 0xE9 is é in Latin-1, but on its own it is not valid UTF-8. fs::write("latin1.txt", [b'c', b'a', b'f', 0xE9, b'\n']).unwrap(); match fs::read_to_string("latin1.txt") { Ok(text) => println!("{text}"), Err(err) => println!("{err:?}"), }}Error { kind: InvalidData, message: "stream did not contain valid UTF-8" }The file was read fine; the error is about the bytes, not the disk, which is why the kind is InvalidData rather than something from the OS. This is the guarantee doing its job: no String in Rust can hold text that isn’t UTF-8, so the conversion that would produce one fails instead. When the file isn’t text at all — an image, an archive, a payload in a fixed-width encoding — fs::read gives you a Vec<u8> and leaves the decoding to you. That has its own article later in this chapter, as does reading the file line by line.
Related
Section titled “Related”- How to handle UTF-8 text — what the guarantee buys, and where it is checked
- What is the difference between String and &str? — the buffer underneath, and why it’s UTF-8
- How to convert a Vec of bytes to a String — the checked conversion, once you have the bytes
- Reading and writing files — the rest of the chapter