File I/O and File System Operations in Rust

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.