Atomicity

#271 Jul 2026

271. fs::rename — Replace a File Atomically, Never Expose a Half-Write

fs::write truncates the file before writing. Crash halfway — or get read halfway — and the world sees a torn file. fs::rename is the atomic swap that fixes it.

The obvious way to save state overwrites in place:

1
2
// Truncates state.json to 0 bytes, then writes.
fs::write("state.json", &json)?;

Between the truncate and the final byte, state.json is incomplete. A concurrent reader gets garbage; a crash or power loss leaves it that way permanently. For a config file, a cache, or anything another process watches, that window is a real bug.

The fix is the classic write-then-rename dance:

1
2
fs::write("state.json.tmp", &json)?;   // build the full file aside
fs::rename("state.json.tmp", "state.json")?; // atomic swap

On every major platform, rename over an existing file is atomic: any observer sees either the old complete file or the new complete file, never a mix and never a missing file. If the process dies before the rename, the old file is untouched and only a .tmp straggler is left behind.

Three details worth knowing:

  • Keep the temp file in the same directory. rename can’t cross filesystems — a temp file in /tmp and a target on another mount fails with an error (ErrorKind::CrossesDevices). Same directory guarantees same filesystem, and keeps the swap atomic.
  • Durability needs one more step. Atomic ≠ flushed. If you need the data to survive power loss, open the temp file yourself and call sync_all() before renaming, instead of using fs::write.
  • Unique temp names matter under concurrency. Two processes writing state.json.tmp will trample each other. Give each writer its own name (PID, random suffix) — and create it with File::create_new (bite 169) so collisions fail loudly.

Same moral as try_exists in bite 270: don’t check-then-act on the filesystem — make the filesystem do the transition in one atomic step.