File I/O
File I/O in Rust is reading and writing files through the std::fs and std::io modules — from one-shot helpers like fs::read_to_string and fs::write to buffered, line-by-line streaming with File and BufReader .
Learn File I/O in our free Rust course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick reference.
Part of the free Rust course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
In this lesson you'll write and read whole files, stream lines with a BufReader , append with OpenOptions , build portable paths, and propagate errors cleanly with the ? operator and io::Result .
What You'll Learn in This Lesson
1️⃣ Whole-File Read and Write
The two workhorses: fs::write(path, data) creates or truncates a file and writes everything in one call, and fs::read_to_string(path) reads a whole text file into a String . Both return io::Result , so a main() -> io::Result<()> can use the ? operator on them.
We build the path from std::env::temp_dir() and push a filename onto it, which works on every OS. The three lines plus their newlines come to 29 bytes.
2️⃣ Streaming Lines with BufReader
For large files, don't slurp the whole thing — open a File , wrap it in a BufReader for buffering, and iterate .lines() . Each item is an io::Result<String> with the newline already stripped, so you ? it to get the String .
3️⃣ Appending with OpenOptions
Both fs::write and File::create overwrite an existing file. To add to the end instead, configure the open with OpenOptions::new().append(true).create(true) — perfect for log files that should grow over time.
Each loop iteration opens the file in append mode and writes one line, so the three events accumulate rather than replacing each other. Swap .append(true) for .write(true).truncate(true) and you'd be back to overwriting.
Your turn. Fill in the blanks marked ____ , then run it.
Mini-Challenge: Average Numbers From a File
Write numbers to a file, then read them back line by line, parse, and average them. Run it with cargo run and check the output.
Common Errors (and the fix)
- ❌ the operator can only be used in a function that returns Result — main returns () . ✅ Fix: make it fn main() -> std::io::Result<()> and end with Ok(()) .
- ❌ no method named found — the BufRead trait isn't imported. ✅ Fix: add use std::io::BufRead; .
- ❌ No such file or directory (os error 2) — you opened a path that doesn't exist. ✅ Fix: write the file first, or check the path.
- ❌ Overwriting when you meant to append — you used fs::write / File::create . ✅ Fix: use OpenOptions::new().append(true) .
Pro Tips
- 💡 Reach for the whole-file helpers first: fs::read_to_string and fs::write handle most needs in one line.
- 💡 Always buffer streaming I/O: wrap File in BufReader / BufWriter to avoid a syscall per line.
- 💡 Build paths with PathBuf : use .push() and temp_dir() instead of hard-coded slashes for portability.
📋 Quick Reference — std::fs
Call
Does
fs::write(p, data)
write a whole file (truncate)
fs::read_to_string(p)
read a whole file to String
File::open(p)
open for reading
BufReader::new(f).lines()
iterate lines
OpenOptions...append(true)
open to append
Frequently Asked Questions
🎉 Lesson Complete!
- ✅ fs::write and fs::read_to_string handle whole files in one call
- ✅ File + BufReader stream a file line by line
- ✅ ? propagates io::Result errors from a -> io::Result<()> function
- ✅ OpenOptions::new().append(true) adds to a file instead of overwriting
- ✅ Build portable paths with temp_dir() and .push()
- ✅ Next lesson: Command-Line Args — reading std::env::args()
Practice quiz
Which call reads an entire text file into a String in one shot?
- fs::read_bytes
- File::open
- fs::read_to_string(path)
- fs::lines
Answer: fs::read_to_string(path). std::fs::read_to_string(path) loads a whole text file into a String in a single call.
What does fs::write(path, contents) do if the file already exists?
- Creates or truncates it, then writes all the bytes
- Appends to it
- Fails with an error
- Renames it
Answer: Creates or truncates it, then writes all the bytes. fs::write creates the file if needed and truncates an existing one before writing everything at once.
What type do most std::fs operations return?
- Option<T>
- Vec<u8>
- bool
- io::Result
Answer: io::Result. They return io::Result (Ok or Err), so you handle errors with ? or match.
To read a file line by line efficiently, what should you wrap a File in?
- a Vec
- a BufReader
- a HashMap
- a String
Answer: a BufReader. Wrap the File in a BufReader and iterate .lines(); buffering reads in chunks instead of one syscall per byte.
Each item yielded by reader.lines() is what?
- An io::Result<String> with the newline stripped
- A plain String
- A char
- A Vec<u8>
Answer: An io::Result<String> with the newline stripped. .lines() yields one io::Result<String> per line, with the trailing newline already removed.
Which trait must be in scope for .lines() to work on a BufReader?
- std::io::Write
- std::fmt::Display
- std::io::BufRead
- std::fs::File
Answer: std::io::BufRead. .lines() comes from the BufRead trait; forgetting to import it causes a 'no method named lines' error.
Which trait must be in scope to use writeln! on a File?
- std::io::Read
- std::io::Write
- std::io::BufRead
- std::fmt::Debug
Answer: std::io::Write. writeln! needs the std::io::Write trait in scope.
How do you append to a file instead of overwriting it?
- fs::write with a flag
- File::create
- fs::read_to_string
- OpenOptions::new().append(true).create(true).open(path)
Answer: OpenOptions::new().append(true).create(true).open(path). OpenOptions with .append(true) opens for adding at the end; .create(true) makes the file if missing.
Why use std::env::temp_dir() and .push() to build a path?
- It is required syntax
- It builds a portable path that works on every OS
- It is faster than strings
- It encrypts the file
Answer: It builds a portable path that works on every OS. Building a PathBuf with temp_dir() and .push() avoids hard-coded slashes, so the code runs on any OS.
What signature lets main use ? on file operations and end with Ok(())?
- fn main()
- fn main() -> bool
- fn main() -> io::Result<()>
- fn main() -> String
Answer: fn main() -> io::Result<()>. fn main() -> io::Result<()> gives a Result return type so ? works, ending the success path with Ok(()).
Continue this course
- Previous: Testing
- Next: Command-Line Args