Short answer
Yes — your understanding is correct on all three points. RStudio provides no transactional semantics, rollback, or cleanup for files written by user code; saveRDS() truncates the destination the moment it opens it, so any interruption destroys the previously good copy. The accepted fix is write-to-temp-then-rename, and it is a user-code responsibility, not an IDE feature. One caveat: atomic rename is only guaranteed within the same filesystem, which matters on Workbench deployments with network mounts.
Why the partial file appears
saveRDS() serializes the object and streams bytes sequentially to the target path. The file is truncated at open time, then filled. If the R process is killed, crashes, runs out of memory, or the session dies mid-write, you get a prefix of a valid serialization — which readRDS() rejects with errors like "unknown input format" or a gzip read error. Nothing in R or RStudio checkpoints the old version first. Note this is not RStudio-specific: plain R and Rscript behave identically, so any fix belongs in your scripts, not the IDE.
The atomic write pattern
save_rds_atomic <- function(object, path) {
dir <- dirname(path)
tmp <- tempfile(pattern = ".tmp-", tmpdir = dir) # same filesystem!
on.exit(unlink(tmp), add = TRUE)
saveRDS(object, tmp)
if (!file.rename(tmp, path)) {
stop("Atomic rename failed for ", path)
}
invisible(path)
}
Key details:
- Same filesystem is mandatory.
tempfile(tmpdir = dirname(path)) ensures the temp file sits next to the target. A rename across filesystems silently degrades to copy-then-delete and loses atomicity. - Check the return value.
file.rename() returns FALSE on failure rather than erroring — this bites on some Windows and network drives. - Rename replaces, it doesn't update. Hard links and already-open handles to the old file still point at the old inode. Rarely an issue for pipeline outputs, but worth knowing.
Network filesystems on Workbench
On POSIX local filesystems, rename within a mount is atomic. On NFS it is generally atomic too, but on some SMB/CIFS mounts and certain Windows-backed shares, rename can fail or behave non-atomically, especially if the destination is open elsewhere. This is the one place I'd verify rather than assume: confirm the temp file and target share a device ID (fs::file_info(c(tmp, path))$device_id should match), and run the kill test below on your actual Workbench storage. If rename proves unreliable there, a fallback is copy-then-verify-checksum-then-switch-a-symlink, but test the simple pattern first.
Existing wrappers
You don't have to hand-roll everything: fs::file_move() gives clearer move semantics, and packages in the targets/pins ecosystem handle versioned outputs, but a small helper like the one above is the common, dependency-free approach and works identically on Desktop and Server/Workbench. The same pattern applies to write.csv() and any other output writer.
Verification: the kill test
- Create a large object (e.g., a multi-GB data frame) and start
save_rds_atomic(x, "out.rds"). - Mid-write, kill the R process from another terminal (
kill <pid>) or via Session > Interrupt/Terminate in RStudio. - Confirm
out.rds either doesn't exist yet or still loads the previous complete object; confirm a .tmp-* leftover exists and is cleaned up. - Repeat with plain
saveRDS(x, "out.rds") to confirm the truncated file fails readRDS() — proving the test actually exercises the failure mode.
If your kill test shows the final path itself getting truncated despite the rename, that points to a cross-filesystem temp file — print tempfile(tmpdir = dirname(path)) and the mount table to check.