Site

File I/O — Rust File Operations

Tutorial S5.0  •  Rust / Learn / StdLib

S5.0 What This Teaches

Rust's file I/O lives in std::fs and std::io. Every file operation returns Result, making error handling explicit. This tutorial covers:

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}");
It returns Result<String, io::Error>. Use it when you need the whole file at once.

S5.3 Reading Line by Line

For large files or when you process lines incrementally, 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());
}
Expected output:
--- 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.txt with 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<()> using read_to_string and fs::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 BufReader for reading and BufWriter for 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
Functions that do I/O should return 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

Relative paths resolve from the process's current working directory. For 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

TermMeaning
File::createOpens a file for writing; creates it if absent, truncates if present
File::openOpens a file for reading; returns Err if not found
OpenOptionsBuilder for fine-grained file open modes (read, write, append, create)
BufReaderWraps a reader with buffering; reduces system calls on line-by-line reads
BufWriterWraps a writer with buffering; flushes in chunks
writeln!Macro that writes a formatted string followed by a newline
fs::read_to_stringReads an entire file into a String in one call
fs::remove_fileDeletes a file
io::Result<T>Shorthand for Result<T, io::Error>