writeFile is not atomic. It truncates the file, then writes; a reader arriving in between sees an empty or half-written file, and a crash leaves one behind for good. Any file another process reads — configuration, a cache, a generated manifest — should be written elsewhere and moved into place.
import { mkdtempDisposable, open, readFile, rename, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
{ // a unique scratch directory, removed on exit
await using tmp = await mkdtempDisposable(path.join(tmpdir(), 'job-'));
console.log('scratch:', path.basename(tmp.path));
}
/** Replace a file's contents so readers never see a half-written file. */
async function writeAtomic(file, data) {
const tmpFile = `${file}.${process.pid}.${Date.now()}.tmp`;
const handle = await open(tmpFile, 'wx');
try {
await handle.writeFile(data);
await handle.sync(); // force the bytes out of the OS cache
} finally {
await handle.close();
}
await rename(tmpFile, file); // atomic within one file system
}
await writeAtomic('config.json', JSON.stringify({ port: 3000 }));
console.log('written:', await readFile('config.json', 'utf8'));
await rm('config.json', { force: true });scratch: job-yu0awM
written: {"port":3000}Never invent a temporary name yourself. mkdtemp asks the operating system for a directory nobody else holds, closing the symlink-attack window a predictable /tmp/myapp-cache leaves open. mkdtempDisposable, added in Node 24.4.0, returns an object with a path and a disposer, so await using removes the tree when the block exits.

Two details make or break the pattern. Put the temporary file in the same directory as its target, not in os.tmpdir(): rename is atomic only inside one file system, and across devices it fails with EXDEV. And call handle.sync() before the rename when the data must survive a power cut, or the rename can reach the disk first. If you would rather not maintain this, write-file-atomic 8.0.0 255 (github.com/npm/write-file-atomic (https://github.com/npm/write-file-atomic 255 ), npm 2,036 i write-file-atomic) is the battle-tested version — npm uses it to rewrite package-lock.json.