Every language
12 linguagens, copy-ready. One at a time with syntax highlighting, or all inline.
JSJavaScript
import fs from 'node:fs';
function writeFileAtomic(path, data) {
const tmp = `${path}.${process.pid}.tmp`; // same directory → same filesystem
const fd = fs.openSync(tmp, 'w');
try {
fs.writeFileSync(fd, data);
fs.fsyncSync(fd); // data hits the disk BEFORE the swap
} finally {
fs.closeSync(fd);
}
try {
fs.renameSync(tmp, path); // atomic within one filesystem
} catch (err) {
fs.unlinkSync(tmp); // never leave the temp file behind
throw err;
}
}The tmp name must live in the TARGET's directory (suffix trick here, mkdtemp/tmpfile for many files in parallel) — /tmp is often another filesystem, and rename across filesystems degrades to a non-atomic copy. graceful-fs / the atomic-write npm wrappers package this whole dance; hand-rolled, the unlink-on-error is the part everyone forgets.
TSTypeScript
import fs from 'node:fs';
import path from 'node:path';
export function atomicWrite(file: string, data: string): void {
const tmp = path.join(
path.dirname(file),
`.${path.basename(file)}.tmp`,
); // provably the same directory
const fd = fs.openSync(tmp, 'w');
try {
fs.writeFileSync(fd, data, 'utf8');
fs.fsyncSync(fd);
} finally {
fs.closeSync(fd);
}
try {
fs.renameSync(tmp, file);
} catch (err) {
try { fs.unlinkSync(tmp); } catch { /* already gone */ }
throw err; // try/catch keeps the rename error AND drops the tmp
}
}path.join(dirname, ...) is the guarantee that keeps the temp file on the target's filesystem. The empty catch around unlinkSync is deliberate — cleanup must not mask the rename failure it follows; the original error is what rethrows.
GoGo
import (
"os"
"path/filepath"
)
func writeFileAtomic(path string, data []byte) error {
tmp, err := os.CreateTemp(filepath.Dir(path), filepath.Base(path)+".tmp*")
if err != nil {
return err
}
tmpName := tmp.Name() // CreateTemp put it in dir → same filesystem
defer os.Remove(tmpName) // no-op once the rename succeeds
if _, err := tmp.Write(data); err != nil {
tmp.Close()
return err
}
if err := tmp.Sync(); err != nil { // flush to disk before the swap
tmp.Close()
return err
}
if err := tmp.Close(); err != nil {
return err
}
return os.Rename(tmpName, path) // atomic within one filesystem
}os.CreateTemp(dir, pattern) takes the TARGET directory — the same-filesystem guarantee comes free, and the pattern's trailing * becomes the random part. It creates with 0600, so the renamed file keeps those perms; Chmod before the rename when the file needs 0644.
RsRust
use std::fs::{self, File};
use std::io::Write;
use std::path::Path;
fn write_atomic(path: &Path, data: &[u8]) -> std::io::Result<()> {
let tmp = path.with_extension("tmp"); // sits beside the target
let mut f = File::create(&tmp)?;
f.write_all(data)?;
f.sync_all()?; // data AND metadata — content is on disk
drop(f); // close before the rename
fs::rename(&tmp, path) // POSIX: atomic within one filesystem
}sync_all flushes data plus metadata; sync_data may skip the metadata, which after a crash can leave the renamed file with a stale size. The tempfile crate is the packaged form — NamedTempFile::new_in(dir) then .persist(path) runs this exact sequence for you.
PHPPHP
function writeFileAtomic(string $path, string $data): void
{
$tmp = $path . '.tmp'; // same directory → rename stays atomic
if (file_put_contents($tmp, $data, LOCK_EX) === false) {
throw new \RuntimeException("write failed: {$tmp}");
}
// fflush() only drains PHP's userspace buffer into the OS page cache —
// PHP core has no fsync (see notes).
if (!rename($tmp, $path)) {
@unlink($tmp);
throw new \RuntimeException("rename failed: {$tmp} → {$path}");
}
}rename() within the same directory is the atomic step and always available. The honest gap: PHP core exposes no fsync — fflush is userspace-only, so a crash right after the rename can still surface an empty file; ext-dio's dio_fsync() on the handle, or OS-level sync, is the durability answer.
PyPython
import os
import tempfile
def write_file_atomic(path: str, data: str) -> None:
d = os.path.dirname(path) or '.'
fd, tmp = tempfile.mkstemp(dir=d, prefix='.tmp-') # same directory
try:
with os.fdopen(fd, 'w', encoding='utf-8') as f:
f.write(data)
f.flush()
os.fsync(f.fileno()) # to disk BEFORE the swap
os.replace(tmp, path) # atomic; overwrites; Windows-safe
except BaseException:
os.unlink(tmp) # cleanup on ANY failure
raiseos.replace, NOT os.rename — replace is atomic AND overwrites the target cross-platform (os.rename refuses to overwrite on Windows). tempfile.NamedTemporaryFile(dir=same_dir, delete=False) is the other spelling of the same-directory temp file; mkstemp just hands you the fd directly.
C#C#
using System.IO;
static void WriteFileAtomic(string path, byte[] data)
{
string tmp = path + ".tmp"; // same directory
using (var fs = new FileStream(tmp, FileMode.Create,
FileAccess.Write, FileShare.None))
{
fs.Write(data, 0, data.Length);
fs.Flush(flushToDisk: true); // fsync, not just userspace
}
File.Move(tmp, path, overwrite: true); // atomic on POSIX
}Flush(true) is the flush-to-disk overload — plain Flush() only drains .NET's buffers into the page cache, which is exactly the durable-but-empty trap. File.Move's overwrite overload (.NET Core 3.0+) is atomic on POSIX; on Windows the replace is not guaranteed atomic, so cross-platform code tolerates a reader seeing the old file a moment longer.
JvJava
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import static java.nio.file.StandardCopyOption.ATOMIC_MOVE;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
static void writeFileAtomic(Path path, byte[] data) throws IOException {
Path dir = path.toAbsolutePath().getParent();
Path tmp = Files.createTempFile(dir, path.getFileName().toString(), ".tmp");
try {
Files.write(tmp, data);
try (FileChannel ch = FileChannel.open(tmp,
StandardOpenOption.READ, StandardOpenOption.WRITE)) {
ch.force(true); // data + metadata, before the swap
}
Files.move(tmp, path, ATOMIC_MOVE, REPLACE_EXISTING);
} catch (AtomicMoveNotSupportedException e) {
// the tmp escaped the target filesystem — surface it, never swallow
throw new IOException("cross-filesystem move: " + tmp, e);
} finally {
Files.deleteIfExists(tmp); // no-op after a successful move
}
}ATOMIC_MOVE throws AtomicMoveNotSupportedException when source and target sit on different filesystems — that exception IS the wrong-directory detector, so catching it to 'fall back to a plain move' silently gives up atomicity. ch.force(true) covers data plus metadata; force(false) can leave the size stale.
SwSwift
import Foundation
func writeFileAtomic(at url: URL, data: Data) throws {
let dir = url.deletingLastPathComponent()
let tmp = dir.appendingPathComponent(".tmp-\(url.lastPathComponent)")
try data.write(to: tmp) // same directory → same filesystem
// Foundation exposes no public fsync; F_FULLFSYNC via raw fcntl on the fd
// is private-API territory. The move below is the atomic step — crash
// durability before it is the documented gap.
if FileManager.default.fileExists(atPath: url.path) {
_ = try FileManager.default.replaceItemAt(url, withItemAt: tmp)
} else {
try FileManager.default.moveItem(at: tmp, to: url)
}
}The rename — replaceItemAt/moveItem on a same-directory URL — is the step that is atomic; POSIX guarantees it within one filesystem. fsync is the honest hole: Foundation has no public API, and Darwin's real-disk F_FULLFSYNC needs a raw fcntl call, so the pre-rename durability window is accepted or handled in C.
KtKotlin
import java.nio.channels.FileChannel
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption.ATOMIC_MOVE
import java.nio.file.StandardCopyOption.REPLACE_EXISTING
import java.nio.file.StandardOpenOption.READ
import java.nio.file.StandardOpenOption.WRITE
fun writeFileAtomic(path: Path, data: ByteArray) {
val dir = path.toAbsolutePath().parent
val tmp = Files.createTempFile(dir, path.fileName.toString(), ".tmp")
try {
Files.write(tmp, data)
FileChannel.open(tmp, READ, WRITE).use { it.force(true) }
Files.move(tmp, path, ATOMIC_MOVE, REPLACE_EXISTING)
} finally {
Files.deleteIfExists(tmp) // no-op after the successful move
}
}Same NIO chain as Java, but use{} closes the channel on every path — a leaked channel pins the fd and can block the rename on Windows. The ATOMIC_MOVE option constant lives in StandardCopyOption (not StandardOpenOption); importing both statically reads cleaner than the vararg-array form.
RbRuby
require 'tempfile'
def write_file_atomic(path, data)
dir = File.dirname(path)
Tempfile.create('.tmp-', dir) do |tmp| # created in the SAME directory
tmp.write(data)
tmp.flush # userspace → OS
tmp.fsync # OS → disk
File.rename(tmp.path, path) # the atomic swap
end
endTempfile.create's SECOND argument is the directory — that is the whole trick, because its default /tmp may be another filesystem. ActiveSupport's File.atomic_write is the packaged idiom; it also restores the original file's mode on the replacement.
ZigZig
const std = @import("std");
fn writeFileAtomic(path: []const u8, data: []const u8) !void {
var buf: [std.fs.max_path_bytes]u8 = undefined;
const tmp = try std.fmt.bufPrint(&buf, "{s}.tmp", .{path});
var f = try std.fs.cwd().createFile(tmp, .{});
defer f.close();
try f.writeAll(data);
try std.posix.fsync(f.handle); // disk BEFORE the swap — order is the contract
try std.posix.rename(tmp, path); // rename(2): atomic within one filesystem
}fsync BEFORE rename is the whole contract — run in the other order, the rename can beat the data to disk and a crash leaves the new name pointing at stale bytes. tmp is path + ".tmp", so it lands in the target's directory by construction; std.posix.rename exposes rename(2), whose same-filesystem atomicity is POSIX-guaranteed.