How do you protect data from crashes mid-write, corruption and backups: atomic writes, journaling, exclusion from backup and data protection?
Predict first
A file is written atomically twice and then flagged as excluded from backup. What do the content and the flag read?
import Foundation
let url = URL(fileURLWithPath: NSTemporaryDirectory() + "t13-\(getpid()).txt")
try Data("v1".utf8).write(to: url, options: .atomic)
try Data("v2".utf8).write(to: url, options: .atomic)
print(String(data: try Data(contentsOf: url), encoding: .utf8)!)
var rv = URLResourceValues()
rv.isExcludedFromBackup = true
var target = url
try target.setResourceValues(rv)
print(try url.resourceValues(forKeys: [.isExcludedFromBackupKey]).isExcludedFromBackup ?? false)
try FileManager.default.removeItem(at: url)
- v1, then false
- v2, then true
- v2, then false
Mental model. Atomic writing is preparing the new page on the side and then swapping it into the binder in one move: at no moment does the binder contain half a page.
A write that is interrupted halfway leaves a torn file: part new, part old, often unreadable. The universal fix is write somewhere else, then swap. For files, Data.write(to:options: .atomic) writes to a temporary file in the same directory and then renames it over the destination in one atomic step, so a reader (or a reader after a crash) sees either the complete old file or the complete new one; the destination ends with the last successful content. Rename is atomic on the same volume, which is why the temporary file must be next to the target (the file-safety question in the Foundation topic covers existence checks and replacement). For databases, atomicity comes from transactions with journaling: SQLite’s rollback journal or WAL guarantees that after a crash the database is either before or after a whole transaction, never in between, as long as you do not defeat it (turning off synchronous trades safety for speed). Core Data and SwiftData sit on this and save atomically per save(). Multi-file state (a database plus a file it refers to) is not atomic across files: write the file first, then the database row that points to it, and tolerate orphans with a cleanup pass.
Add defence in depth. Validate on read: version your formats, checksum important files, treat a decode failure as “start fresh or restore from backup”, not as a crash, and keep the previous good copy for important state. Run PRAGMA integrity_check after an unclean shutdown for critical databases. Backups (iCloud and Finder) include Documents and Application Support but not Caches or tmp; mark large re-creatable files you must keep outside Caches with URLResourceValues.isExcludedFromBackup = true (read back with resourceValues(forKeys:)), so they do not bloat the user’s backup or break the iCloud quota; do not exclude user-created data. Data protection encrypts files with keys tied to the passcode: the class completeUnlessOpen or the default completeUntilFirstUserAuthentication decides when the file is readable while the device is locked, and a background task that needs a file after the screen locks must use a class that allows it (a too-strict class makes background reads fail). Sync, restore and reinstall create more shapes of bad state, so make launch code defensive: missing files, partial data, newer-than-supported versions. Finally, keep a crash-safe order for destructive actions (delete the record, then the file; never the reverse) and log integrity failures so you can see them in the field.
Each atomic write replaces the whole file, so the last one wins. The resource value reads back as set.
Common traps
- Overwriting a file in place. A crash mid-write corrupts it; use an atomic write or write-then-rename.
- Excluding user-created documents from backup, or leaving large caches in Documents. Exclude only re-creatable data, and put caches in Caches.
They might follow up with
- Why is the temporary file written next to the target? A rename within the same volume is atomic; across volumes it is a copy.
- What does the file protection class change? When the file is readable while the device is locked.