ਇੱਕ ਕੌਂਫਿਗ ਫਾਈਲ ਜੋ ਪੰਜ ਸੈਕਿੰਡ ਪਹਿਲਾਂ ਸਹੀ ਕੰਮ ਕਰ ਰਹੀ ਸੀ, ਹੁਣ ਟੁੱਟੇ ਹੋਏ JSON ਦਾ 347-ਬਾਈਟ ਦਾ ਟੁਕੜਾ ਬਣ ਗਈ ਹੈ। ਤੁਹਾਡਾ CLI ਟੂਲ ਸ਼ੁਰੂ ਨਹੀਂ ਹੋਵੇਗਾ। ਉਹ ਯੂਜ਼ਰ, ਜਿਸਨੇ ਸਿਰਫ਼ ਇਸ ਲਈ Ctrl-C ਦਬਾਇਆ ਸੀ ਕਿਉਂਕਿ ਅੱਪਡੇਟ ਉਮੀਦ ਤੋਂ ਵੱਧ ਸਮਾਂ ਲੈ ਰਿਹਾ ਸੀ, ਹੁਣ ਇੱਕ ਅਜਿਹੇ ਐਰਰ ਸਟੈਕ ਟ੍ਰੇਸ (error stack trace) ਨੂੰ ਦੇਖ ਰਿਹਾ ਹੈ ਜਿਸਦੀ ਉਸਨੇ ਮੰਗ ਨਹੀਂ ਕੀਤੀ ਸੀ। ਲਿਖਣ (write) ਤੋਂ ਪਹਿਲਾਂ ਮੌਜੂਦ ਦੋ ਕਿਲੋਬਾਈਟ ਦੀ ਵੈਧ ਕੌਂਫਿਗਰੇਸ਼ਨ ਖ਼ਤਮ ਹੋ ਗਈ ਹੈ, ਅਤੇ ਉਸਦੀ ਜਗ੍ਹਾ ਅੱਧੇ-ਅਧੂਰੇ ਪ੍ਰਿੰਟ ਕੀਤੇ ਰਸੀਦ ਵਰਗਾ ਡਿਜੀਟਲ ਟੁਕੜਾ ਰਹਿ ਗਿਆ ਹੈ।
ਇਹ ਇਸ ਲਈ ਹੁੰਦਾ ਹੈ ਕਿਉਂਕਿ writeFile ਐਟੋਮਿਕ (atomic) ਨਹੀਂ ਹੈ। ਇਹ ਮੌਜੂਦਾ ਪਾਥ (path) ਨੂੰ ਖੋਲ੍ਹਦਾ ਹੈ, ਇਸਨੂੰ ਟਰੰਕੇਟ (truncate) ਕਰਦਾ ਹੈ, Node ਤੋਂ ਕਰਨਲ ਦੇ ਪੇਜ ਕੈਸ਼ (kernel’s page cache) ਵਿੱਚ ਡਾਟਾ ਸਟ੍ਰੀਮ ਕਰਦਾ ਹੈ, ਅਤੇ ਅੰਤ ਵਿੱਚ ਫਾਈਲ ਡਿਸਕ੍ਰਿਪਟਰ (file descriptor) ਨੂੰ ਬੰਦ ਕਰ ਦਿੰਦਾ ਹੈ। ਜਿਸ ਪਲ ਟਰੰਕੇਸ਼ਨ ਹੁੰਦਾ ਹੈ, ਪੁਰਾਣਾ ਕੰਟੈਂਟ ਪਹਿਲਾਂ ਹੀ ਖ਼ਤਮ ਹੋ ਚੁੱਕਾ ਹੁੰਦਾ ਹੈ। ਉਸ ਟਰੰਕੇਸ਼ਨ ਅਤੇ ਅੰਤਿਮ close ਦੇ ਵਿਚਕਾਰ ਦਾ ਸਮਾਂ ਕਮਜ਼ੋਰੀ ਦਾ ਇੱਕ ਖੇਤਰ (window of vulnerability) ਹੁੰਦਾ ਹੈ। ਉਸ ਸਮੇਂ ਦੌਰਾਨ ਇੱਕ SIGINT, ਪਾਵਰ ਕੱਟ, ਜਾਂ ਲੈਪਟਾਪ ਦਾ ਢੱਕਣ ਬੰਦ ਹੋ ਜਾਣਾ ਫਾਈਲਸਿਸਟਮ ਨੂੰ ਇੱਕ ਟੁੱਟੇ-ਫੁੱਟੇ ਹਾਲਤ ਵਿੱਚ ਛੱਡ ਦਿੰਦਾ ਹੈ। ਭਾਵੇਂ Node Promise ਨੂੰ resolved ਦੱਸਦਾ ਹੈ, ਫਿਰ ਵੀ ਓਪਰੇਟਿੰਗ ਸਿਸਟਮ ਮੈਮੋਰੀ ਵਿੱਚ ਰਾਈਟਸ (writes) ਨੂੰ ਬਫਰ ਕਰ ਰਿਹਾ ਹੋ ਸਕਦਾ ਹੈ। ਕਨਵੀਨੀਅੰਸ ਮੈਥਡ (Convenience methods) ਉਸ ਖਾਲੀ ਸਮੇਂ ਨੂੰ ਲੁਕਾ ਦਿੰਦੇ ਹਨ, ਪਰ ਉਹ ਉਸਨੂੰ ਖ਼ਤਮ ਨਹੀਂ ਕਰਦੇ।
ਇਸਦਾ ਹੱਲ ਮੌਜੂਦਾ ਥਾਂ 'ਤੇ ਲਿਖਣਾ ਨਹੀਂ ਹੈ। ਹੱਲ ਲਿਖਣ ਦੇ ਕੰਮ ਨੂੰ ਪਬਲਿਸ਼ ਕਰਨ ਦੇ ਕੰਮ ਤੋਂ ਵੱਖ ਕਰਨਾ ਹੈ।
ਇੱਕ ਸਿਬਲਿੰਗ (sibling) ਫਾਈਲ ਵਿੱਚ ਲਿਖੋ, ਫਿਰ ਸਵੈਪ (swap) ਕਰੋ
ਇਸ ਭਰੋਸੇਯੋਗ ਪੈਟਰਨ ਵਿੱਚ ਪੰਜ ਕਦਮ ਹਨ। ਇਹਨਾਂ ਵਿੱਚੋਂ ਕੋਈ ਵੀ ਗੁੰਝਲਦਾਰ ਨਹੀਂ ਹੈ, ਪਰ ਮਿਲ ਕੇ ਇਹ ਫੇਲ੍ਹ ਹੋਣ ਦੇ ਖ਼ਤਰੇ (failure window) ਨੂੰ ਪੂਰੀ ਸਟ੍ਰੀਮਿੰਗ ਰਾਈਟ ਤੋਂ ਘਟਾ ਕੇ ਸਿਰਫ਼ ਇੱਕ ਸਿੰਗਲ ਫਾਈਲਸਿਸਟਮ ਮੈਟਾਡਾਟਾ ਆਪਰੇਸ਼ਨ ਤੱਕ ਸੀਮਤ ਕਰ ਦਿੰਦੇ ਹਨ।
ਪਹਿਲਾਂ, ਪੂਰੇ ਪੇਲੋਡ (payload) ਨੂੰ ਮੈਮੋਰੀ ਵਿੱਚ ਸੀਰੀਅਲਾਈਜ਼ (serialize) ਕਰੋ। ਇਹ ਕਿਸੇ ਵੀ ਟੈਂਪਰੇਰੀ ਫਾਈਲ ਨੂੰ ਬਣਾਉਣ ਤੋਂ ਪਹਿਲਾਂ ਕਰੋ। ਜੇਕਰ JSON.stringify ਕੋਈ ਐਕਸੈਪਸ਼ਨ (exception) ਦਿੰਦਾ ਹੈ ਕਿਉਂਕਿ ਕਿਸੇ ਨੇ ਸਰਕੂਲਰ ਆਬਜੈਕਟ (circular object) ਪਾਸ ਕੀਤਾ ਹੈ, ਤਾਂ ਤੁਸੀਂ ਚਾਹੁੰਦੇ ਹੋ ਕਿ ਡਿਸਕ ਨੂੰ ਛੂਹਣ ਤੋਂ ਪਹਿਲਾਂ ਉਹ ਐਕਸੈਪਸ਼ਨ ਉੱਪਰ ਆ ਜਾਵੇ।
ਦੂਜਾ, ਸੀਰੀਅਲਾਈਜ਼ ਕੀਤੇ ਡਾਟਾ ਨੂੰ ਟਾਰਗੇਟ ਵਾਲੇ ਡਾਇਰੈਕਟਰੀ ਵਿੱਚ ਹੀ ਇੱਕ ਟੈਂਪਰੇਰੀ ਫਾਈਲ ਵਿੱਚ ਲਿਖੋ। ਇੱਕ ਰੈਂਡਮਾਈਜ਼ਡ (randomized) ਨਾਮ ਦੀ ਵਰਤੋਂ ਕਰੋ ਤਾਂ ਜੋ ਦੋ ਇਕੱਠੇ ਚੱਲ ਰਹੇ ਪ੍ਰੋਗਰਾਮ ਆਪਸ ਵਿੱਚ ਨਾ ਟਕਰਾਉਣ। ਟੈਂਪ ਫਾਈਲ ਨੂੰ ਉਸੇ ਡਾਇਰੈਕਟਰੀ ਵਿੱਚ ਰੱਖਣਾ ਮਹੱਤਵਪੂਰਨ ਹੈ ਕਿਉਂਕਿ rename ਸਿਰਫ਼ ਇੱਕ ਸਿੰਗਲ ਫਾਈਲਸਿਸਟਮ ਦੇ ਅੰਦਰ ਹੀ ਐਟੋਮਿਕ ਹੁੰਦਾ ਹੈ। ਜੇਕਰ ਤੁਹਾਡੀ ਟੈਂਪ ਫਾਈਲ ਕਿਸੇ ਵੱਖਰੇ ਪਾਰਟੀਸ਼ਨ 'ਤੇ ਹੈ, ਤਾਂ ਓਪਰੇਟਿੰਗ ਸਿਸਟਮ ਕਾਪੀ-ਅੰਡ-ਡਿਲੀਟ (copy-and-delete) ਸੀਕੁਐਂਸ ਦੀ ਵਰਤੋਂ ਕਰਦਾ ਹੈ, ਜੋ ਆਪਣੇ ਆਪ ਵਿੱਚ ਫੇਲ੍ਹ ਹੋਣ ਦੇ ਨਵੇਂ ਤਰੀਕੇ ਲਿਆਉਂਦਾ ਹੈ ਅਤੇ ਫਿਰ ਇਹ ਐਟੋਮਿਕ ਨਹੀਂ ਰਹਿੰਦਾ।
ਤੀਜਾ, ਕਰਨਲ ਨੂੰ ਉਸ ਟੈਂਪਰੇਰੀ ਫਾਈਲ ਨੂੰ ਫਿਜ਼ੀਕਲ ਸਟੋਰੇਜ ਵਿੱਚ ਫਲਸ਼ (flush) ਕਰਨ ਲਈ ਕਹੋ। Node ਦਾ fsync, ਜੋ ਇੱਥੇ ਇੱਕ ਫਾਈਲਹੈਂਡਲ (filehandle) 'ਤੇ sync ਮੈਥਡ ਵਜੋਂ ਦਿੱਤਾ ਗਿਆ ਹੈ, ਉਦੋਂ ਤੱਕ ਰੁਕਦਾ ਹੈ ਜਦੋਂ ਤੱਕ ਬਫਰ ਮੈਟਲ (metal) 'ਤੇ ਡਾਊਨ ਨਹੀਂ ਹੋ ਜਾਂਦੇ। ਇਹ ਹੌਲੀ ਹੈ, ਪਰ ਕੌਂਫਿਗ ਰਾਈਟਸ ਇੰਨੀ ਘੱਟ ਵਾਰ ਹੁੰਦੇ ਹਨ ਕਿ ਇਸਦੀ ਸੁਰੱਖਿਆ (durability) ਲਈ ਕੁਝ ਮਿਲੀਸੈਕਿੰਡ ਦਾ ਸਮਾਂ ਦੇਣਾ ਜਾਇਜ਼ ਹੈ।
ਚੌਥਾ, ਟੈਂਪਰੇਰੀ ਫਾਈਲ ਨੂੰ ਮੌਜੂਦਾ ਪਾਥ 'ਤੇ ਰੀਨੇਮ (rename) ਕਰੋ। POSIX ਸਿਸਟਮਾਂ ਅਤੇ Windows ਦੋਵਾਂ 'ਤੇ, ਇਹ ਕਮਿਟ ਪੁਆਇੰਟ (commit point) ਹੈ। ਮੌਜੂਦਾ ਪਾਥ ਨੂੰ ਖੋਲ੍ਹਣ ਵਾਲੇ ਰੀਡਰਸ (readers) ਜਾਂ ਤਾਂ ਪੂਰੀ ਪੁਰਾਣੀ ਫਾਈਲ ਦੇਖਣਗੇ ਜਾਂ ਪੂਰੀ ਨਵੀਂ ਫਾਈਲ। ਅਜਿਹਾ ਕੋਈ ਸਮਾਂ ਨਹੀਂ ਹੁੰਦਾ ਜਦੋਂ ਕੋਈ ਰੀਡਰ ਪਾਥ ਨੂੰ ਖੋਲ੍ਹੇ ਅਤੇ ਅਧੂਰੇ ਲਿਖੇ ਹੋਏ ਬਫਰ ਨੂੰ ਦੇਖੇ।
ਪੰਜਵਾਂ, ਪੇਰੈਂਟ ਡਾਇਰੈਕਟਰੀ ਨੂੰ ਸਿੰਕ (sync) ਕਰੋ। ਇਹ ਇੱਕ ਬਾਰੀਕ ਐਜ ਕੇਸ (edge case) ਨੂੰ ਸੰਭਾਲਦਾ ਹੈ। ਰੀਨੇਮ ਡਾਇਰੈਕਟਰੀ ਐਂਟਰੀ ਨੂੰ ਅੱਪਡੇਟ ਕਰਦਾ ਹੈ, ਪਰ ਡਾਇਰੈਕਟਰੀ ਮੈਟਾਡਾਟਾ ਖੁਦ ਕਰਨਲ ਦੇ ਪੇਜ ਕੈਸ਼ ਵਿੱਚ ਹੋ ਸਕਦਾ ਹੈ। ਸਫਲ ਰੀਨੇਮ ਤੋਂ ਬਾਅਦ ਅਚਾਨਕ ਪਾਵਰ ਜਾਣ ਨਾਲ ਕਈ ਵਾਰ ਫਾਈਲਸਿਸਟਮ ਅਜਿਹੀ ਸਥਿਤੀ ਵਿੱਚ ਰਹਿ ਸਕਦਾ ਹੈ ਜਿੱਥੇ ਨਵੇਂ inode ਰੈਫਰੈਂਸ ਨੂੰ ਸਹੀ ਤਰ੍ਹਾਂ ਰਿਕਾਰਡ ਨਹੀਂ ਕੀਤਾ ਗਿਆ ਸੀ। ਡਾਇਰੈਕਟਰੀ ਨੂੰ ਸਿੰਕ ਕਰਨ ਨਾਲ ਉਹ ਮੈਟਾਡਾਟਾ ਅੱਪਡੇਟ ਡਿਸ
The cleanup in the catch block is intentionally defensive. If anything throws after the filehandle is opened, the code attempts to close the handle and remove the temporary file, swallowing any secondary errors so the original exception propagates cleanly. You do not want a permission error during cleanup to mask the real bug that caused the failure.
Where this pattern stops helping
Atomic file replacement prevents torn writes. It does not prevent lost updates. If two instances of your CLI read the same config simultaneously, both perform their edits in memory, both write fresh temp files, and both execute their renames, the second rename wins. The first process did not observe the second process’s changes. Depending on your application, this could mean a user adds a setting in one terminal and a different user removes it in another, with the final file reflecting only the last writer.
If your tool needs to support concurrent mutators, you need a coordination mechanism on top of the atomic write. An advisory lock file works for simple cases. Version vectors or a monotonic revision number inside the config itself can help detect collisions so the second writer can retry. These add complexity, and complexity is where bugs hide.
That is why the boundary matters. A single JSON blob that one process updates occasionally is a fine candidate for an atomic file write. Once you find yourself managing multiple records, enforcing schemas, or worrying about concurrent mutations, you have outgrown the filesystem. SQLite exists for exactly this reason. It gives you atomic transactions, rollback journals, and proper handling of concurrent readers and writers, all inside a single host-local file. A clever file protocol is not a database, and you should not spend maintenance budget pretending otherwise.
The real takeaway
The next time you reach for writeFile inside a CLI tool, pause. Serialization is not the hard part. Durability is. Config files are too small to stream and too important to truncate. Write the entire payload to a hidden sibling, flush it, commit it with a rename, and tell the directory about it. Your users can press Ctrl-C, yank the power cord, or close their laptop lid. When the machine comes back, the file will either contain the old world or the new one. Nothing in between.
