Reading and writing text files

File input and output in Rust is a set of small functions that return io::Result. The simplest is fs::write and its partner fs::read_to_string, and the streaming form wraps a File in a buffer. Every call may fail, and the error is the same Result the previous lessons used.

This lesson writes a file, reads it back, walks it line by line, appends to it and reports a missing one. The paths are relative to the directory the program runs in, which the page commands make explicit. The last section is the error for a file method whose trait was not imported.

Writing and reading a file

fs::write creates or truncates a file and writes the bytes of the string. fs::read_to_string reads the whole file into a new String, which is fine for a configuration or a report and wasteful for a gigabyte of logs.

The program writes two lines and reads them back. The length is twenty-three bytes, which counts the two newlines, and the text prints unchanged.

use std::fs; fn main() { fs::write("notes.txt", "first line\nsecond line\n").unwrap(); let text = fs::read_to_string("notes.txt").unwrap(); println!("{} bytes", text.len()); print!("{text}"); }

rustc --edition 2024 write_read.rs
./write_read

23 bytes
first line
second line

Both functions panic here on an error because the program calls unwrap; a tool that must survive a bad path handles the Result instead, which the fourth section shows.

Line by line

BufReader reads ahead in large blocks, and lines() splits the stream on newlines without loading the whole file. Each item is an io::Result because a line can fail to decode as UTF-8.

The program opens the file written by the previous section and prints each line with its number. The two lines from the example come out in order.

use std::fs::File; use std::io::{BufRead, BufReader}; fn main() { let file = File::open("notes.txt").unwrap(); let reader = BufReader::new(file); for (index, line) in reader.lines().enumerate() { println!("{}: {}", index + 1, line.unwrap()); } }

rustc --edition 2024 lines.rs
./lines

1: first line
2: second line

The line ending is removed by lines, which is why the print does not double the newline. The lesson on CSV parsing builds its rows on this loop.

Appending

fs::write replaces the file, so an append needs OpenOptions with append(true) and create(true). The handle implements Write, and the bytes go at the end.

The program appends a third line and counts the lines of the result. The count is three, which proves the two earlier lines survived.

use std::fs::OpenOptions; use std::io::Write; fn main() { let mut file = OpenOptions::new() .append(true) .create(true) .open("notes.txt") .unwrap(); file.write_all(b"third line\n").unwrap(); let text = std::fs::read_to_string("notes.txt").unwrap(); println!("{} lines", text.lines().count()); }

rustc --edition 2024 append.rs
./append

3 lines

The same builder can open for reading, writing and creating at once, and every option is off by default. The lesson on binary files uses the same type for raw bytes.

A missing file

A path that does not exist is an ordinary Err with an ErrorKind of NotFound. Matching the result turns the failure into a message instead of a panic.

The program tries to read a file that is not there and prints the error. The message is the operating system text with the error number in brackets.

use std::fs; fn main() { match fs::read_to_string("no-such-file.txt") { Ok(text) => println!("{} bytes", text.len()), Err(error) => println!("error: {error}"), } }

rustc --edition 2024 missing.rs
./missing

error: No such file or directory (os error 2)

The ErrorKind is the part a program should match, because the message is localised by the system. The lesson on error types turns it into an error of its own.

A trait that is not in scope

read_to_string is a method of the Read trait, not of File itself. Calling it without use std::io::Read is E0599, and the help writes the import.

The program opens a file and calls the method without the import. The compiler names the trait, which is the same pattern the lesson on printing showed for Write.

use std::fs::File; fn main() { let mut file = File::open("notes.txt").unwrap(); let mut text = String::new(); file.read_to_string(&mut text).unwrap(); println!("{text}"); }

rustc --edition 2024 trait_err.rs

error[E0599]: no method named `read_to_string` found for struct `File` in the current scope
--> trait_err.rs:6:10
|
6 | file.read_to_string(&mut text).unwrap();
| ^^^^^^^^^^^^^^
|
--> /rustc/31fca3adb283cc9dfd56b49cdee9a96eb9c96ffd/library/std/src/io/mod.rs:973:7
|
= note: the method is available for `File` here
|
= help: items from traits can only be used if the trait is in scope
help: trait `Read` which provides `read_to_string` is implemented but not in scope; perhaps you want to import it
|
1 + use std::io::Read;
|
help: there is a method `read_to_end` with a similar name
|
6 - file.read_to_string(&mut text).unwrap();
6 + file.read_to_end(&mut text).unwrap();
|

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0599`.

The same happens with BufRead for lines and with Write for write_all. The import is the whole fix.

Exercises

  1. Write a file with three numbers and read them back into a vector.
  2. Count the lines of a file with BufReader and again with read_to_string.
  3. Append a timestamp-free line to a file twice and check the count.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What does fs::read_to_string return?

    Answer

    A Result with the whole file as a String

    The function reads everything and reports an io error if it cannot.

  2. Why use BufReader for a large file?

    Answer

    It reads in blocks instead of one byte per call

    The buffer turns many small reads into a few large ones, which is much faster.

  3. Which options append to a file?

    Answer

    append(true) and create(true)

    Append moves the writes to the end and create makes the file when it is absent.

  4. What kind of error does a missing file produce?

    Answer

    An io error with the NotFound kind

    The standard library reports the operating system error, and its kind is NotFound.

  5. What does this program print? use std::fs; fn main() { fs::write("notes.txt", "first line\nsecond line\n").unwrap(); let text = fs::read_to_string("notes.txt").unwrap(); println!("{} bytes", text.len()); }

    Answer

    23 bytes

    The two lines and the two newlines add up to twenty-three bytes.

  6. What does this program print? use std::fs::File; use std::io::{BufRead, BufReader}; fn main() { File::create("lines.txt").unwrap(); std::fs::write("lines.txt", "a\nb\n").unwrap(); let file = File::open("lines.txt").unwrap(); for (index, line) in BufReader::new(file).lines().enumerate() { println!("{}: {}", index + 1, line.unwrap()); } }

    Answer

    1: a / 2: b

    The two lines print with numbers starting at one.

  7. What does this program print? use std::fs; fn main() { fs::write("data.txt", "one\ntwo\n").unwrap(); let text = fs::read_to_string("data.txt").unwrap(); println!("{}", text.lines().count()); }

    Answer

    2

    The text has two lines, and the trailing newline does not add an empty one.

  8. What does this program print? use std::fs; fn main() { match fs::read_to_string("missing-file.txt") { Ok(text) => println!("{} bytes", text.len()), Err(error) => println!("error: {}", error), } }

    Answer

    error: No such file or directory (os error 2)

    The file does not exist, and the error carries the operating system message.

  9. What is the first line of the compiler output for this program? use std::fs::File; fn main() { let mut file = File::open("notes.txt").unwrap(); let mut text = String::new(); file.read_to_string(&mut text).unwrap(); println!("{text}"); }

    Answer

    error[E0599]: no method named read_to_string found for struct File in the current scope

    The method belongs to the Read trait, and the help adds the import.

  10. What does this program print? use std::fs; fn main() { fs::write("count.txt", "a\nb\nc").unwrap(); let text = fs::read_to_string("count.txt").unwrap(); println!("{}", text.lines().count()); }

    Answer

    3

    There are three lines, and the last one has no trailing newline.

The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev