How to write to a file
Short answer: std::fs::write(path, contents) writes the whole buffer to a file in one call: it creates the file if it isn’t there, and truncates it to zero bytes first if it is. It returns io::Result<()> — the call writes every byte or tells you it didn’t — so a missing parent directory or a permission problem comes back as an Err you can inspect.
From a fresh project — cargo new write-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 then reads, so it runs on its own and in any order; in your own code the file is already there and you delete the write line. Paths are relative to the directory you run from, which under cargo run is the project root.
Write the whole file in one call
Section titled “Write the whole file in one call”use std::fs;
fn main() -> std::io::Result<()> { fs::write("poem.txt", "Leaves are falling\nsoftly on the path\n")?; let text = fs::read_to_string("poem.txt")?; print!("{text}"); println!("{} bytes", text.len()); Ok(())}Leaves are fallingsoftly on the path38 bytesOne call opens the file, writes every byte and closes it — no buffer to size, no loop. The contents argument is anything that can be viewed as a slice of bytes: &str and &[u8] both go in:
use std::fs;
fn main() -> std::io::Result<()> { // The first eight bytes of a PNG file; 0x89 is not valid UTF-8, and write does not care. fs::write("sample.bin", [0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A])?; let bytes = fs::read("sample.bin")?; println!("{} bytes", bytes.len()); Ok(())}8 bytesThere is no encoding step and no check: a &str goes out as the bytes it is made of, a &[u8] as it stands. Reading it back as text is a decision made on the way in — a String can only hold valid UTF-8.
The file already exists? It gets truncated
Section titled “The file already exists? It gets truncated”fs::write opens with create-and-truncate, so an existing file is emptied before the new bytes land. Nothing of the old contents survives — not even past the end of the new ones:
use std::fs;
fn main() -> std::io::Result<()> { fs::write("log.txt", "first line\nsecond line\nthird line\n")?; println!("{} bytes after the first write", fs::metadata("log.txt")?.len()); fs::write("log.txt", "short\n")?; let text = fs::read_to_string("log.txt")?; print!("{text}"); println!("{} bytes after the second", fs::metadata("log.txt")?.len()); Ok(())}34 bytes after the first writeshort6 bytes after the secondThis is the behaviour that catches people out: rewriting a config file with a shorter body leaves no tail of the old one. Adding to a file instead of replacing it is a different call — OpenOptions with append set — and that has its own page later in this chapter.
What happens on error
Section titled “What happens on error”The failure everyone hits first is a parent directory that isn’t there:
use std::fs;use std::io::ErrorKind;use std::os::unix::fs::PermissionsExt;
fn main() { match fs::write("no-such-dir/out.txt", "hello") { Ok(()) => println!("written"), Err(err) => { println!("{err}"); println!("{err:?}"); println!("NotFound: {}", err.kind() == ErrorKind::NotFound); } }
// src exists in a Cargo project, and it is a directory. match fs::write("src", "hello") { Ok(()) => println!("written"), Err(err) => println!("{err:?}"), }
// A file you may read but not write; removed first so the block runs twice. let _ = fs::remove_file("locked.txt"); fs::write("locked.txt", "first").unwrap(); fs::set_permissions("locked.txt", fs::Permissions::from_mode(0o444)).unwrap(); match fs::write("locked.txt", "second") { Ok(()) => println!("written"), Err(err) => println!("{err:?}"), }}No such file or directory (os error 2)Os { code: 2, kind: NotFound, message: "No such file or directory" }NotFound: trueOs { code: 21, kind: IsADirectory, message: "Is a directory" }Os { code: 13, kind: PermissionDenied, message: "Permission denied" }Each failure has a kind: NotFound when a parent directory isn’t there, IsADirectory when the path is one, PermissionDenied when the file is read-only. fs::write creates the file, not the directories on the way to it; create_dir_all does that, later in this chapter. Match on err.kind() when a failure is acceptable, and propagate the rest with ?.
Writing in pieces
Section titled “Writing in pieces”When the bytes arrive in pieces, or the buffer is too big to hold at once, open the file and write to it yourself:
use std::fs::{self, File};use std::io::{self, Write};
fn main() -> io::Result<()> { let mut file = File::create("report.txt")?; writeln!(file, "line one")?; writeln!(file, "line two")?; println!("{} bytes", fs::metadata("report.txt")?.len()); Ok(())}18 bytesFile::create is the same create-or-truncate open fs::write does, minus the writing: an existing file is emptied first, and everything you write lands from the start. write_all, which is what writeln! calls underneath, loops until every byte is in, so a short write surfaces as an error instead of silent data loss. File is unbuffered — each call is a system call — so many small pieces want a BufWriter around the file. Nothing needs flushing when you are done: the bytes already went to the OS, and the file closes when file goes out of scope.
Related
Section titled “Related”- How to read a file into a String — reading back what you just wrote, and the UTF-8 check
- How to read a file into bytes — the reader for a payload that isn’t text
- How to handle UTF-8 text — the guarantee behind what a
&stron disk means - Reading and writing files — the rest of the chapter