File

#169 May 2026

169. File::create_new — Atomic 'Create Only If It Doesn't Exist'

You want to write a config file, a lockfile, or a “did we run yet” sentinel — but only if it isn’t already there. The if path.exists() { … } else { File::create(path) } pattern looks fine until two processes hit it at the same time. There’s a one-line fix sitting in std::fs.

The naive guard is a textbook TOCTOU race: between the moment you check existence and the moment you call create, another process can slip in and put a file there. You’ll then happily truncate their work.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
use std::fs::{self, File};
use std::path::Path;

fn write_once_racy(path: &Path) -> std::io::Result<File> {
    if path.exists() {
        return Err(std::io::Error::new(
            std::io::ErrorKind::AlreadyExists,
            "already there",
        ));
    }
    // Window of vulnerability: another process can create the file here.
    File::create(path) // truncates if it now exists
}

File::create_new (stable since 1.77) collapses both steps into a single syscall — O_CREAT | O_EXCL on Unix, CREATE_NEW on Windows — so the kernel decides the winner:

1
2
3
4
5
6
use std::fs::File;
use std::path::Path;

fn write_once(path: &Path) -> std::io::Result<File> {
    File::create_new(path)
}

If the file already exists, you get back an io::Error with ErrorKind::AlreadyExists and nothing on disk is touched. That’s the whole behaviour — and it’s the same whether one process or fifty are racing for the same path.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
use std::fs::{self, File};
use std::io::{ErrorKind, Write};

let path = std::env::temp_dir().join("rustbites-169.lock");
let _ = fs::remove_file(&path); // start clean

// First call wins and gets a writable handle.
let mut first = File::create_new(&path).expect("first create");
first.write_all(b"owner=me").unwrap();

// Second call fails — no truncation, no clobber.
let err = File::create_new(&path).unwrap_err();
assert_eq!(err.kind(), ErrorKind::AlreadyExists);

fs::remove_file(&path).unwrap();

For the equivalent guarantee on an existing handle you’d previously reach for OpenOptions::new().write(true).create_new(true).open(path) — that still works, and File::create_new is just the shorthand when you want the default “write, create-new, truncate-off” combo.

Use it for lockfiles, idempotent setup steps, “did we already write the manifest” checks, and anywhere the existence test and the create were a single logical step pretending to be two.