Rust provides powerful and safe file input/output through the std::fs module. You can read from and write to files, create directories, iterate through folder contents, and handle file system errors, all with strong type safety and predictable behavior.
When I first started working with files in Rust, I was surprised at how ergonomic and safe the experience felt. Coming from JavaScript, where file operations sometimes failed silently or crashed at runtime, Rust’s error handling and ownership system made everything more deliberate and less error-prone.
In this article, you will learn how to read and write files, create directories, check for file existence, and loop through directories using the standard library.
Reading a File to a String
The easiest way to read a file is using fs::read_to_string:
use std::fs;
fn main() -> std::io::Result<()> {
let content = fs::read_to_string("data.txt")?;
println!("File contents: {}", content);
Ok(())
}
This function reads the entire file into a String and returns a Result.
If the file does not exist, or cannot be read, the function returns an error. You can use ? to propagate the error, or handle it with match.
Writing to a File
To write to a file, use fs::write:
use std::fs;
fn main() -> std::io::Result<()> {
fs::write("output.txt", "Hello, file!")?;
Ok(())
}
This creates the file if it does not exist, or overwrites it if it does.
If you want to append instead of overwrite, use OpenOptions.
Appending to a File
To append, open the file in append mode:
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, "New log entry")?;
Ok(())
}
You can also enable read, write, or truncate based on your needs.
Creating and Checking Directories
To create a directory:
use std::fs;
fn main() -> std::io::Result<()> {
fs::create_dir("my_folder")?;
Ok(())
}
To create nested directories:
fs::create_dir_all("my_folder/nested/more")?;
Check if a path exists:
use std::path::Path;
if Path::new("data.txt").exists() {
println!("File exists!");
}
Reading a File Line by Line
You can use BufReader for efficient reading:
use std::fs::File;
use std::io::{self, BufRead};
fn main() -> io::Result<()> {
let file = File::open("data.txt")?;
let reader = io::BufReader::new(file);
for line in reader.lines() {
println!("{}", line?);
}
Ok(())
}
This is useful for large files or streaming input.
Listing Files in a Directory
Use fs::read_dir to loop through a directory:
use std::fs;
fn main() -> std::io::Result<()> {
for entry in fs::read_dir(".")? {
let entry = entry?;
let path = entry.path();
println!("{:?}", path);
}
Ok(())
}
This gives you access to file names, metadata, and file types.
Removing Files and Directories
Delete a file:
fs::remove_file("old.txt")?;
Delete an empty directory:
fs::remove_dir("my_folder")?;
Delete a directory with contents:
fs::remove_dir_all("my_folder")?;
Always check return values to avoid silent failures.
Handling File I/O Errors
Rust encourages handling I/O errors explicitly:
let result = fs::read_to_string("missing.txt");
match result {
Ok(content) => println!("Read: {}", content),
Err(e) => eprintln!("Failed: {}", e),
}
For custom behavior, use pattern matching on e.kind().
When to Use File vs BufReader
- Use fs::read_to_string for small files and quick loading
- Use BufReader for large files or line-by-line processing
- Use OpenOptions for precise control over writing
Summary
Rust’s std::fs module makes file and directory operations straightforward and safe. Whether you are reading, writing, appending, or navigating folders, you get predictable, type-safe behavior with detailed error messages. Learning these I/O tools prepares you for real-world applications from CLI tools to web servers and beyond.
Rust Intermediate Concepts
- Generic Types in Functions and Structs
- Implementing Traits for Custom Behavior in Rust
- Trait Bounds and Constraints in Rust
- Lifetimes in Rust
- Using Closures (Anonymous Functions) in Rust
- The Iterator Trait and .next() in Rust
- Higher Order Iterator Methods: map, filter, and fold in Rust
- Using impl Trait for Simplicity in Rust
- Advanced Collections: HashSet and BTreeMap in Rust
- Custom Error Types and the Error Trait in Rust
- Option and Result Combinators in Rust
- Writing Unit Tests in Rust
- Integration Testing in Rust
- Logging in Rust with the log Crate
- Cargo Tips and Tricks for Rust Projects
- CLI Argument Parsing with Clap in Rust
- File I/O and File System Operations in Rust
- Running External Commands in Rust
- Make HTTP Requests in Rust with Reqwest
- JSON Serialization and Deserialization in Rust with Serde
- Building a Weather CLI in Rust
- Date and Time Handling in Rust with Chrono
- Using Regular Expressions in Rust with the regex Crate
- Memory Management in Rust
- Understanding Borrow Checker Errors in Rust
- Interacting with Databases in Rust
- Building a Todo List CLI in Rust with SQLite
- Concurrency in Rust