In Rust, file operations are typically handled using the std::fs module for high-level functions and std::io for more granular control over streams. These modules provide abstractions for common tasks such as reading lines, writing data, and managing file handles in a safe and efficient manner.

This tutorial covers:

  • Reading the contents of a file
  • Writing to a file (appending and overwriting)
  • Iterating over lines in a file
  • Handling I/O errors using Result and ?

Let’s dive into concrete examples and best practices.

Reading a File

To read the entire contents of a file, the std::fs::read_to_string function is the most straightforward option. It returns a Result<String, std::io::Error> and automatically closes the file after reading.

use std::fs;

fn read_file() -> std::io::Result<()> {
    let contents = fs::read_to_string("example.txt")?;
    println!("File contents:\n{}", contents);
    Ok(())
}

For larger files or when you need more control, use File and BufReader:

use std::fs::File;
use std::io::{BufRead, BufReader};

fn read_file_with_buffer() -> std::io::Result<()> {
    let file = File::open("example.txt")?;
    let reader = BufReader::new(file);

    for (index, line) in reader.lines().enumerate() {
        let line = line?;
        println!("Line {}: {}", index + 1, line);
    }

    Ok(())
}

Using BufReader improves performance by reducing the number of system calls.

Writing to a File

Writing to a file can be done using std::fs::write, which replaces the file contents, or std::fs::OpenOptions for more control over write modes (e.g., appending).

use std::fs;

fn write_file() -> std::io::Result<()> {
    let data = "Hello from Rust!\n";
    fs::write("output.txt", data)?;
    Ok(())
}

To append to a file instead of overwriting it:

use std::fs::OpenOptions;
use std::io::Write;

fn append_to_file() -> std::io::Result<()> {
    let mut file = OpenOptions::new()
        .append(true)
        .create(true)
        .open("output.txt")?;

    file.write_all(b"Appended line\n")?;
    Ok(())
}

Error Handling

Proper error handling is essential in Rust. The ? operator propagates errors by returning early if an Err variant is encountered. This ensures that functions that return Result types can be composed cleanly.

Compare the following two approaches:

MethodDescriptionExample
unwrap()Panics on errorlet data = fs::read_to_string("file.txt").unwrap();
? operatorReturns Err on errorlet data = fs::read_to_string("file.txt")?;

Always prefer ? in functions that return Result or Option to avoid panics in production code.

Idiomatic File Iteration

When reading a file line by line, using BufRead::lines() with for loops is idiomatic and efficient.

use std::fs::File;
use std::io::{BufRead, BufReader};

fn process_lines() -> std::io::Result<()> {
    let file = File::open("input.txt")?;
    let reader = BufReader::new(file);

    for line in reader.lines() {
        let line = line?;
        // Process the line
        if line.contains("error") {
            println!("Found error in line: {}", line);
        }
    }

    Ok(())
}

This pattern is especially useful for log processing or parsing structured text.

Summary of Common File I/O Operations

TaskFunctionDescription
Read entire filefs::read_to_stringReads file into a String
Write to filefs::writeOverwrites file with given data
Open for writingOpenOptions::new().write(true).open()Opens file for writing
Append to fileOpenOptions::new().append(true).open()Appends data to file
Read line by lineBufRead::lines()Iterates over lines of a file

Learn more with useful resources