~/blog · 3 May 2026 · 4 min read
Writing files without breaking them: the tmp + rename trick
A process that dies mid fs.writeFile leaves a corrupted JSON. It's not an edge case — it happens on deploys, restarts, power cuts. There's a two-line fix.
When I built FileSystemAdapter for cron-scheduler-ts, the first thing I did was a plain fs.writeFile. It worked locally, tests passed. Then I thought: what happens if the process dies right in the middle of writing? And I got worried.
fs.writeFile opens the file, truncates it to zero bytes, and then writes the new content. If the process dies between the truncation and the complete write, the file has 0 bytes or half a JSON. On the next startup, JSON.parse throws, the scheduler can't load state, and you lose all persisted jobs. For a 2am backup task that wouldn't run again until the next day — that's a real problem.
The problem with direct writes
// ❌ Not safe — there's a window where data.json ends up corrupted
// If the process dies here ↓ the file has 0 bytes or broken JSON
await fs.writeFile("data.json", JSON.stringify(state, null, 2));
// If this fails halfway → data.json is corrupted foreverWhy rename() is different
On POSIX systems — Linux, macOS — rename() is an atomic operation. The operating system guarantees that, from the filesystem's perspective, either the rename happens completely or it doesn't happen at all. No intermediate state. This means if you write to a temp file and then rename it to the final path, the reader always sees either the complete old file or the complete new one. Never a broken one.
// ✅ Atomic write — data.json never ends up corrupted
async function saveState(path: string, state: unknown) {
const tmpPath = path + ".tmp";
// Write to the temp file — if it fails, data.json stays intact
await fs.writeFile(tmpPath, JSON.stringify(state, null, 2), "utf8");
// rename() is atomic: at this point, data.json = complete new state
await fs.rename(tmpPath, path);
}atomic write
When you actually need this
The honest answer: not always. For logs, append is safe enough and far more efficient. For files that are read once at startup and represent critical state — config, job state, user metadata — the tmp + rename pattern is essential. On Windows it also works since Vista: the MoveFileEx API has the same atomic behaviour.
What I can say with confidence: since implementing it in FileSystemAdapter, I haven't had a single corrupted file report. And the package restarts constantly during tests. Two extra lines, zero problems.