S5.0 What This Teaches
std::fs and std::io. Every file
operation returns Result, making error handling explicit. This tutorial
covers:
- Writing a file with
File::createandwriteln! - Reading an entire file with
fs::read_to_string - Reading line by line with
BufReader - Appending to an existing file
- Checking existence and deleting a file
S5.1 Writing a File
File::create creates or truncates a file. writeln! writes
a line with a trailing newline. Both return Result - use ?
to propagate errors:
use std::fs::File;
use std::io::{self, Write};
fn write_sample(path: &str) -> io::Result<()> {
let mut file = File::create(path)?;
writeln!(file, "line one")?;
writeln!(file, "line two")?;
writeln!(file, "line three")?;
Ok(())
}
io::Result<()> is shorthand for Result<(), io::Error>.
The ? operator converts io::Error values and returns them
to the caller automatically.
S5.2 Reading an Entire File
fs::read_to_string is the simplest approach when the file fits in
memory:
use std::fs;
let content = fs::read_to_string("sample.txt")?;
print!("{content}");
Result<String, io::Error>. Use it when you need the
whole file at once.
S5.3 Reading Line by Line
BufReader
avoids loading everything into memory:
use std::fs::File;
use std::io::{self, BufRead, BufReader};
fn read_lines(path: &str) -> io::Result<Vec<String>> {
let file = File::open(path)?;
let reader = BufReader::new(file);
reader.lines().collect()
}
reader.lines() is an iterator of io::Result<String>.
.collect() with return type io::Result<Vec<String>>
short-circuits on the first error.
S5.4 Appending to a File
OpenOptions controls how a file is opened. append(true)
moves the write cursor to the end without truncating:
use std::fs;
use std::io::Write;
let mut file = fs::OpenOptions::new()
.append(true)
.open("sample.txt")?;
writeln!(file, "line four")?;
S5.5 Checking Existence and Deleting
fs::metadata returns Ok if the path exists and the process
can stat it:
println!("exists: {}", fs::metadata("sample.txt").is_ok());
fs::remove_file("sample.txt")?;
println!("exists: {}", fs::metadata("sample.txt").is_ok());
S5.6 Example - All Together
// FileIO - demonstrates reading and writing files using std::fs and std::io.
use std::fs::{self, File};
use std::io::{self, BufRead, BufReader, Write};
fn write_sample(path: &str) -> io::Result<()> {
let mut file = File::create(path)?;
writeln!(file, "line one")?;
writeln!(file, "line two")?;
writeln!(file, "line three")?;
Ok(())
}
fn read_whole(path: &str) -> io::Result<String> {
fs::read_to_string(path)
}
fn read_lines(path: &str) -> io::Result<Vec<String>> {
let file = File::open(path)?;
let reader = BufReader::new(file);
reader.lines().collect()
}
fn main() {
let path = "sample.txt";
// --- write a file ---
println!("--- write ---");
write_sample(path).expect("write failed");
println!("wrote {path}");
// --- read entire file into a String ---
println!("--- read_to_string ---");
let content = read_whole(path).expect("read failed");
print!("{content}");
// --- read line by line ---
println!("--- read lines ---");
let lines = read_lines(path).expect("read failed");
for (i, line) in lines.iter().enumerate() {
println!("{i}: {line}");
}
// --- append to a file ---
println!("--- append ---");
{
let mut file = fs::OpenOptions::new()
.append(true)
.open(path)
.expect("open failed");
writeln!(file, "line four").expect("append failed");
}
println!("line count: {}", read_lines(path).unwrap().len());
// --- check existence and remove ---
println!("--- exists / remove ---");
println!("exists: {}", fs::metadata(path).is_ok());
fs::remove_file(path).expect("remove failed");
println!("exists after remove: {}", fs::metadata(path).is_ok());
}
--- write ---
wrote sample.txt
--- read_to_string ---
line one
line two
line three
--- read lines ---
0: line one
1: line two
2: line three
--- append ---
line count: 4
--- exists / remove ---
exists: true
exists after remove: false
S5.7 Exercise
Exercise
- Write a program that creates
numbers.txtwith the integers 1 through 10, one per line. Read it back and print the sum. - Write a function
copy_file(src: &str, dst: &str) -> io::Result<()>usingread_to_stringandfs::write. Verify the copy is identical by reading both back. - Read a text file line by line, filter out blank lines, and write the non-blank
lines to a second file. Use
BufReaderfor reading andBufWriterfor writing.
S5.8 Common Mistakes
Not propagating errors from file operations
let content = fs::read_to_string("file.txt").unwrap();
// panics if the file is missing
io::Result and use ?
so the caller decides how to handle failure.
Opening with File::create when you meant to append
let file = File::create("log.txt")?; // truncates existing content!
File::create truncates the file if it exists. Use
OpenOptions::new().append(true).open(path) when you want to add to an
existing file.
Forgetting to flush a BufWriter
BufWriter holds data in memory and flushes in chunks. Drop order
flushes automatically in Rust, but an early return Err(...) before
the drop can leave data unwritten. Call .flush()? explicitly when
in doubt.
Relative paths and working directory surprises
cargo run that is the crate root (where Cargo.toml
lives). Use std::env::current_dir() to inspect it when paths do
not resolve as expected.
S5.9 Key Terms
| Term | Meaning |
|---|---|
| File::create | Opens a file for writing; creates it if absent, truncates if present |
| File::open | Opens a file for reading; returns Err if not found |
| OpenOptions | Builder for fine-grained file open modes (read, write, append, create) |
| BufReader | Wraps a reader with buffering; reduces system calls on line-by-line reads |
| BufWriter | Wraps a writer with buffering; flushes in chunks |
| writeln! | Macro that writes a formatted string followed by a newline |
| fs::read_to_string | Reads an entire file into a String in one call |
| fs::remove_file | Deletes a file |
| io::Result<T> | Shorthand for Result<T, io::Error> |