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.
Reading an entire file
Section titled “Reading an entire file”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).
Using ? to simplify error handling
Section titled “Using ? to simplify error handling”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}"), }}Writing to a file
Section titled “Writing to a file”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.
Appending to a file
Section titled “Appending to a file”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.
Reading line by line
Section titled “Reading line by line”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 aString, returning aResult.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 inmain, instead of matching every call.- None of this runs in the Playground — test file operations in a local project.
Quick check
Section titled “Quick check”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