A config file that worked five seconds ago is now a 347-byte fragment of broken JSON. Your CLI tool will not start. The user, who simply pressed Ctrl-C because the update was taking longer than expected, is now staring at an error stack trace they did not ask for. The two kilobytes of valid configuration that existed before the write are gone, replaced by the digital equivalent of a half-printed receipt.

This happens because writeFile is not atomic. It opens the existing path, truncates it, streams data from Node into the kernel’s page cache, and eventually closes the file descriptor. The moment the truncation happens, the old content is already gone. Everything between that truncation and the final close is a window of vulnerability. A SIGINT, a power outage, or a laptop lid slamming shut during that window leaves the filesystem holding a truncated mess. Even if Node reports the Promise as resolved, the operating system may still be buffering writes in memory. Convenience methods hide that gap, but they do not remove it.

The fix is not to write in place. The fix is to separate the act of writing from the act of publishing.

Write to a sibling, then swap

The reliable pattern has five steps. None of them are complicated, but together they move the failure window from an entire streaming write down to a single filesystem metadata operation.

First, serialize the entire payload in memory. Do this before you create any temporary file. If JSON.stringify throws because someone passed a circular object, you want that exception to bubble up before you touch the disk.

Second, write the serialized data to a temporary file located in the same directory as the target. Use a randomized name so two concurrent runs do not collide. Keeping the temp file in the same directory matters because rename is only atomic within a single filesystem. If your temp file lives on a different partition, the operating system falls back to a copy-and-delete sequence, which introduces its own failure modes and is no longer atomic.

Third, ask the kernel to flush that temporary file to physical storage. Node’s fsync, exposed here as the sync method on a filehandle, blocks until the buffers are down on the metal. This is slow, but config writes happen rarely enough that the durability is worth the milliseconds.

Fourth, rename the temporary file over the original path. On both POSIX systems and Windows, this is the commit point. Readers opening the original path will see either the complete old file or the complete new file. There is no moment in time where a reader can open the path and observe a half-written buffer.

Fifth, sync the parent directory. This catches a subtle edge case. The rename updates the directory entry, but the directory metadata itself might sit in the kernel’s page cache. Sudden power loss after a successful rename can sometimes leave the filesystem in a state where the new inode reference was never recorded durably. Syncing the directory forces that metadata update to disk and seals the transaction.

A concrete Node.js implementation

Here is what that pattern looks like in practice using only the Node.js standard library:

import { open, rename, rm } from "node:fs/promises";
import { dirname, basename, join } from "node:path";
import { randomUUID } from "node:crypto";

export async function writeJsonAtomic(path, value) {
  const directory = dirname(path);
  const temporary = join(directory, `.${basename(path)}.${randomUUID()}.tmp`);
  const body = `${JSON.stringify(value, null, 2)}\n`;
  let handle;

  try {
    handle = await open(temporary, "wx", 0o600);
    await handle.writeFile(body, "utf8");
    await handle.sync();
    await handle.close();
    handle = undefined;

    await rename(temporary, path);

    const directoryHandle = await open(directory, "r");
    try {
      await directoryHandle.sync();
    } finally {
      await directoryHandle.close();
    }
  } catch (error) {
    if (handle) await handle.close().catch(() => {});
    await rm(temporary, { force: true }).catch(() => {});
    throw error;
  }
}

A few details in this code are worth attention.

The wx flag means “write, but fail if the file already exists.” This guards against a UUID collision or an abandoned temp file from a previous crashed process. If someone has dropped a malicious file where your temp file should be, you will hear about it immediately rather than overwriting whatever is there.

The 0o600 permission mask creates the temp file with owner-read and owner-write only. Config files frequently hold secrets, API tokens, or private repository URLs. There is no reason to let other users on the system peek at the temporary file while it is being prepared.

Notice the separate sync calls on the file and then on the directory. Many developers skip the directory sync because it feels redundant. It is not. Ext4, APFS, and NTFS all handle directory updates differently, but they share a common habit of batching metadata writes for performance. If you care about surviving power loss, the directory sync is the final seal.

catch block-ലെ ക്ലീനപ്പ് (cleanup) ബോധപൂർവ്വം പ്രതിരോധപരമായ രീതിയിലാണ് (defensive) ക്രമീകരിച്ചിരിക്കുന്നത്. ഫയൽ ഹാൻഡിൽ (filehandle) തുറന്നതിന് ശേഷം എന്തെങ്കിലും പിശക് (exception) സംഭവിച്ചാൽ, ഹാൻഡിൽ ക്ലോസ് ചെയ്യാനും താൽക്കാലിക ഫയൽ (temporary file) നീക്കം ചെയ്യാനും കോഡ് ശ്രമിക്കുന്നു. ഇതിനിടയിൽ മറ്റ് പിശകുകൾ (secondary errors) ഉണ്ടായാലും അവ അവഗണിക്കപ്പെടുന്നു (swallowing), അങ്ങനെ യഥാർത്ഥ പിശക് (original exception) തടസ്സമില്ലാതെ പുറത്തുവരുന്നു. ക്ലീനപ്പ് സമയത്തുണ്ടാകുന്ന ഒരു പെർമിഷൻ എറർ (permission error), യഥാർത്ഥ പിശകിനെ മറച്ചുവെക്കുന്നത് നിങ്ങൾ ആഗ്രഹിക്കില്ല.

ഈ രീതി എവിടെയാണ് ഫലപ്രദമല്ലാതാകുന്നത്

അറ്റോമിക് ഫയൽ റീപ്ലേസ്‌മെന്റ് (Atomic file replacement) 'torn writes' തടയുന്നുണ്ടെങ്കിലും, 'lost updates' തടയുന്നില്ല. നിങ്ങളുടെ CLI-യുടെ രണ്ട് ഇൻസ്റ്റൻസുകൾ ഒരേസമയം ഒരേ കോൺഫിഗറേഷൻ (config) വായിക്കുകയും, രണ്ടും മെമ്മറിയിൽ മാറ്റങ്ങൾ വരുത്തുകയും, പുതിയ ടെംപ് ഫയലുകൾ എഴുതുകയും, രണ്ടും റീനേം (rename) ചെയ്യുകയും ചെയ്താൽ, രണ്ടാമത്തെ റീനേം ആണ് വിജയിക്കുക. ആദ്യത്തെ പ്രോസസ്സിന് രണ്ടാമത്തെ പ്രോസസ്സിന്റെ മാറ്റങ്ങൾ കാണാൻ കഴിഞ്ഞില്ല. നിങ്ങളുടെ ആപ്ലിക്കേഷനെ ആശ്രയിച്ച്, ഇതിനർത്ഥം ഒരു ഉപയോക്താവ് ഒരു ടെർമിനലിൽ ഒരു സെറ്റിംഗ് ചേർക്കുകയും മറ്റൊരു ഉപയോക്താവ് അത് നീക്കം ചെയ്യുകയും ചെയ്തേക്കാം, അപ്പോൾ അവസാന ഫയലിൽ അവസാനമായി മാറ്റം വരുത്തിയ ആളുടെ വിവരങ്ങൾ മാത്രമേ ഉണ്ടാവുകയുള്ളൂ.

നിങ്ങളുടെ ടൂളിന് ഒരേസമയം ഒന്നിലധികം മാറ്റങ്ങൾ (concurrent mutators) വരുത്താൻ സാധിക്കണമെങ്കിൽ, അറ്റോമിക് റൈറ്റിന് പുറമെ ഒരു കോർഡിനേഷൻ സംവിധാനം (coordination mechanism) കൂടി ആവശ്യമാണ്. ലളിതമായ സാഹചര്യങ്ങളിൽ ഒരു അഡ്വൈസറി ലോക്ക് ഫയൽ (advisory lock file) മതിയാകും. കോൺഫിഗറിനുള്ളിൽ തന്നെ വെർഷൻ വെക്ടറുകളോ (Version vectors) മോണോട്ടോണിക് റിവിഷൻ നമ്പറുകളോ (monotonic revision number) ഉപയോഗിക്കുന്നത് മാറ്റങ്ങൾ തമ്മിലുള്ള സംഘർഷം (collisions) കണ്ടെത്താൻ സഹായിക്കും, അങ്ങനെ രണ്ടാമത്തെ എഴുത്തുകാരന് വീണ്ടും ശ്രമിക്കാവുന്നതാണ്. ഇവ സങ്കീർണ്ണത വർദ്ധിപ്പിക്കുന്നു, സങ്കീർണ്ണതയിലാണ് ബഗുകൾ ഒളിച്ചിരിക്കുന്നത്.

അതുകൊണ്ടാണ് അതിർവരമ്പുകൾ പ്രധാനമാകുന്നത്. ഇടയ്ക്കിടെ ഒരു പ്രോസസ്സ് മാത്രം അപ്‌ഡേറ്റ് ചെയ്യുന്ന ഒരു സിംഗിൾ JSON ബ്ലോബ് (JSON blob) അറ്റോമിക് ഫയൽ റൈറ്റിന് അനുയോജ്യമാണ്. ഒന്നിലധികം റെക്കോർഡുകൾ കൈകാര്യം ചെയ്യേണ്ടി വരുമ്പോഴോ, സ്കീമകൾ (schemas) നടപ്പിലാക്കേണ്ടി വരുമ്പോഴോ, അല്ലെങ്കിൽ ഒരേസമയം വരുന്ന മാറ്റങ്ങളെക്കുറിച്ച് ആശങ്കപ്പെടുമ്പോഴോ, ഫയൽ സിസ്റ്റം നിങ്ങൾക്ക് മതിയാകില്ല. കൃത്യമായി ഈ ആവശ്യത്തിനായാണ് SQLite നിലവിലുള്ളത്. ഇത് അറ്റോമിക് ട്രാൻസാക്ഷനുകൾ (atomic transactions), റോൾബാക്ക് ജേണലുകൾ (rollback journals), ഒരേസമയം വായിക്കുന്നവരെയും എഴുതുന്നവരെയും ശരിയായി കൈകാര്യം ചെയ്യാനുള്ള സംവിധാനം എന്നിവ ഒരു ലോക്കൽ ഫയലിനുള്ളിൽ തന്നെ നൽകുന്നു. ഒരു മികച്ച ഫയൽ പ്രോട്ടോക്കോൾ ഒരിക്കലും ഒരു ഡാറ്റാബേസ് അല്ല, അത് അങ്ങനെയാണെന്ന് വരുത്തിത്തീർക്കാൻ നിങ്ങൾ സമയം കളയരുത്.

യഥാർത്ഥ പാഠം

അടുത്ത തവണ ഒരു CLI ടൂളിനുള്ളിൽ writeFile ഉപയോഗിക്കാൻ ശ്രമിക്കുമ്പോൾ ഒന്ന് നിർത്തുക. സീരിയലൈസേഷൻ (Serialization) അത്ര പ്രയാസമുള്ള കാര്യമല്ല. ഡ്യൂറബിലിറ്റി (Durability) ആണ് പ്രധാനം. കോൺഫിഗർ ഫയലുകൾ സ്ട്രീം ചെയ്യാൻ വളരെ ചെറുതും എന്നാൽ ട്രങ്കേറ്റ് (truncate) ചെയ്യാൻ കഴിയാത്തവിധം പ്രധാനപ്പെട്ടതുമാണ്. മുഴുവൻ ഡാറ്റയും ഒരു ഹിഡൻ ഫയലിലേക്ക് (hidden sibling) എഴുതുക, അത് ഫ്ലഷ് (flush) ചെയ്യുക, റീനേം (rename) ചെയ്ത് കമിറ്റ് ചെയ്യുക, ഒപ്പം ഡയറക്ടറിയെ അതിനെക്കുറിച്ച് അറിയിക്കുകയും ചെയ്യുക. നിങ്ങളുടെ ഉപയോക്താക്കൾക്ക് Ctrl-C അമർത്താനോ, പവർ കോർഡ് വലിച്ചൂരിടാനോ, അല്ലെങ്കിൽ ലാപ്‌ടോപ്പ് അടയ്ക്കാനോ സാധിക്കും. മെഷീൻ വീണ്ടും പ്രവർത്തിപ്പിക്കുമ്പോൾ, ഫയലിൽ ഒന്നുകിൽ പഴയ വിവരങ്ങൾ ഉണ്ടാകും അല്ലെങ്കിൽ പുതിയ വിവരങ്ങൾ ഉണ്ടാകും. ഇവ രണ്ടിനും ഇടയിലുള്ള അവസ്ഥ ഉണ്ടാവില്ല.