Skip to content
Select theme

How to read a file line by line

Short answer: Wrap the file in a BufReader and iterate lines(): for line in BufReader::new(file).lines(). Each item is an io::Result<String> with the newline already stripped. A bare File can’t do this — lines() is a method on the BufRead trait, and File does not implement it.

From a fresh project — cargo new read-demo, then replace the contents of src/main.rs with each block below and run it with cargo run. The project name shows up only in cargo’s own output; nothing else on this page depends on it. Each block writes the file it reads before reading it, so it runs on its own; in your own code the file is already there and you delete that line.

Leaves are falling
softly on the path

lines() yields one io::Result<String> per line, newline stripped, and stops at the end of the file. The ? inside the loop turns a read error into an early return from main, the same way the whole-file version does it; BufRead has to be in scope for the method to resolve. The path is relative to the directory you run the program from, which under cargo run is the project root.

File implements Read, but read_line and lines are methods on BufRead, and File does not implement that trait. Calling one directly is a compile error, not a runtime surprise:

error[E0599]: no method named `read_line` found for struct `File` in the current scope
--> src/main.rs:6:10
|
6 | file.read_line(&mut line).unwrap();
| ^^^^^^^^^
|
help: there is a method `read_at` with a similar name, but with different arguments
--> /Users/rango/.rustup/toolchains/1.99.0-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/os/unix/fs.rs:61:5
|
61 | fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result<usize>;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
For more information about this error, try `rustc --explain E0599`.

The read_at suggestion is a red herring — it’s a Unix positional read, not a line reader. Importing BufRead doesn’t fix the call either, because the trait has to be implemented for the type, not just in scope. The reason is buffering: File is unbuffered, so every read is a system call, and reading line by line straight from it would mean a syscall per read. The standard library doesn’t offer it — the line-splitting methods sit on the buffered reader instead. BufReader wraps any Read and reads ahead in 8 KiB chunks (its default capacity), keeping the bytes in memory until you consume them. A line longer than the buffer is fine: read_line keeps refilling until it finds the newline.

[one]
[two]
[three]
3 lines

lines() strips \r\n as well as \n, so Windows files come out clean. A last line with no newline is still yielded, and a trailing newline does not produce an empty line at the end — the count matches the lines an editor shows. When you only need the number, .count() gives it without collecting them.

A line that isn’t valid UTF-8 fails with the same InvalidData kind the whole-file version gives, one line at a time — and because the error arrives per line, the loop decides what to do with it:

Error { kind: InvalidData, message: "stream did not contain valid UTF-8" }

? would abandon the whole loop on the first bad line; match lets you skip it, count it, or keep the good lines. The guarantee behind the error is on the UTF-8 page.