Skip to content

Rust File I/O

Reading and writing files is one of the first places where everything you’ve learned about Result and the ? operator stops being theoretical — almost every file operation can fail (the file might not exist, you might not have permission), so the standard library makes you deal with that directly.

The simplest way to read a file’s contents into a String:

use std::fs;
fn main() {
let contents = fs::read_to_string("hello.txt");
match contents {
Ok(text) => println!("{text}"),
Err(e) => println!("Couldn't read file: {e}"),
}
}

fs::read_to_string returns a Result<String, std::io::Error>Ok with the text if it worked, Err with a reason if it didn’t (file missing, no permission, and so on).

Matching on every file operation gets repetitive fast. If you’re inside a function that itself returns a Result, the ? operator (from the Result page) cleans this up a lot:

use std::fs;
use std::io;
fn read_file(path: &str) -> Result<String, io::Error> {
let contents = fs::read_to_string(path)?; // returns early on failure
Ok(contents)
}
fn main() {
match read_file("hello.txt") {
Ok(text) => println!("{text}"),
Err(e) => println!("Error: {e}"),
}
}
use std::fs;
fn main() {
let result = fs::write("output.txt", "Hello, file!");
match result {
Ok(()) => println!("Wrote successfully"),
Err(e) => println!("Failed to write: {e}"),
}
}

fs::write creates the file if it doesn’t exist, and overwrites it completely if it does — keep that in mind if you need to add to a file instead of replacing it.

For adding to an existing file rather than overwriting it, use OpenOptions:

use std::fs::OpenOptions;
use std::io::Write;
fn main() -> std::io::Result<()> {
let mut file = OpenOptions::new()
.append(true)
.create(true)
.open("log.txt")?;
writeln!(file, "A new log line")?;
Ok(())
}

Notice main itself returns Result here — that’s a pattern worth knowing: when main returns a Result, you can use ? directly inside it instead of wrapping everything in a match.

For a large file, reading the whole thing into memory at once isn’t always ideal. Reading line by line instead:

use std::fs::File;
use std::io::{BufRead, BufReader};
fn main() -> std::io::Result<()> {
let file = File::open("hello.txt")?;
let reader = BufReader::new(file);
for line in reader.lines() {
println!("{}", line?);
}
Ok(())
}
  • fs::read_to_string(path) reads a whole file into a String, returning a Result.
  • fs::write(path, contents) writes a file, overwriting it if it already exists.
  • OpenOptions::new().append(true) lets you add to a file instead of replacing it.
  • main() -> std::io::Result<()> lets you use ? directly in main, instead of matching every call.
  • None of this runs in the Playground — test file operations in a local project.

Quick check

1. What happens if you call fs::write on a path that already exists?

2. How do you add to a file instead of overwriting it?

3. What does it enable when main itself returns std::io::Result<()>?

4. What does the ? operator do when fs::read_to_string(path)? fails?

5. Why read a large file line by line with BufReader instead of fs::read_to_string?

Score: 0 / 5