Skip to content

/commands/rust/read-file-line-by-line

rust read a file line by line (BufReader::lines and read_to_string)

verified onenvironmentlocal · windows 10.0.26200 arm64 · GNU bash 5.3.15 (Git Bash) · rustc 1.98.1 (48a229cea 2026-09-01)updated
tool
Rust
versions
Rust 1.0+, fs::read_to_string: Rust 1.26+
os
any

To read a file line by line in Rust, open it with File::open, wrap it in BufReader, and iterate over .lines(): each item is an io::Result<String> without the line ending. For small files, std::fs::read_to_string(path)? reads everything into one String, and .lines() on that string iterates without further I/O errors.

Both approaches

bash
printf 'alpha\r\nbeta\n\ngamma' > input.txt
cat > lines.rs <<'EOF'
use std::fs::{self, File};
use std::io::{self, BufRead, BufReader};

fn main() -> io::Result<()> {
    // Streaming: constant memory, works for large files
    let reader = BufReader::new(File::open("input.txt")?);
    for (number, line) in reader.lines().enumerate() {
        let line = line?;
        println!("{:>2}: {:?}", number + 1, line);
    }

    // Whole file in memory: simplest for small files
    let text = fs::read_to_string("input.txt")?;
    let non_empty: Vec<&str> = text.lines().filter(|l| !l.is_empty()).collect();
    println!("non-empty lines: {:?}", non_empty);

    // A missing file is an io::Error, propagated by ?
    match File::open("missing.txt") {
        Ok(_) => println!("unexpected"),
        Err(e) => println!("error kind: {:?}", e.kind()),
    }
    Ok(())
}
EOF
rustc --edition 2024 lines.rs && ./lines

Output

 1: "alpha"
 2: "beta"
 3: ""
 4: "gamma"
non-empty lines: ["alpha", "beta", "gamma"]
error kind: NotFound

Both methods strip \n and \r\n, so Windows line endings need no extra handling; the last line is returned even without a final newline, and an empty line comes back as an empty string.

Choosing a method

MethodItem typeWhen to use
BufReader::new(file).lines()io::Result<String>Large files or streams; one line in memory at a time
fs::read_to_string(path)?.lines()&str borrowed from one StringSmall files; simplest code, a single error check
reader.read_line(&mut buf)usize (bytes read)Reuse one String buffer, keep the line ending
reader.split(b'\n')io::Result<Vec<u8>>Files that are not valid UTF-8
io::stdin().lock().lines()io::Result<String>Read standard input line by line

Pitfalls

  • ? needs a Result-returning function. Using ? inside fn main() without -> io::Result<()> (or another Result) fails to compile with E0277. Declare the return type as in the example.
  • Unwrapping each line. for line in reader.lines() yields Results; printing line directly prints Ok("..."). Use let line = line?; or handle the error.
  • Borrowed lines. Lines from read_to_string(...).lines() borrow from the String; keep the String alive while you use them, or convert with .to_string(). See String vs &str.
  • Numbers in lines must be parsed: see parse a string to an int.

Confirm it worked

Print the line count and compare it with wc -l (which counts newline characters, so it is one lower when the file has no trailing newline, as in the example).

FAQ

Does lines() keep the newline characters?

No. Each String returned by BufRead::lines has the trailing \n, or \r\n, removed. Use read_line in a loop if you need the line endings.

What happens with a file that is not valid UTF-8?

lines() and read_to_string return an error of kind InvalidData for the first invalid sequence. Read bytes with BufRead::split(b'\n') or fs::read, then convert with String::from_utf8_lossy.

Should I wrap the File in a BufReader?

Yes. lines() is a method of the BufRead trait, which File does not implement. BufReader adds a buffer, so the file is read in large chunks instead of one system call per line.

Sources