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 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 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.
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.
📋 Quick Reference — std::fs
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(()).