astrid-runtime/astrid · error · io::Error
AlreadyExists
AlreadyExists
Error message
a private-file write transaction is already pending
What it means
Before publishing a new journal, write_private_file_transaction_journal checks whether the journal file (.astrid-private-write.transaction.json) already exists. If it does, another private-file write transaction is pending (likely from a crashed or concurrent writer) and starting a new one would corrupt recovery state, so the call fails with AlreadyExists.
Source
Thrown at crates/astrid-core/src/platform_fs/windows/private_file.rs:201
Ok(journal)
}
pub(super) fn write_private_file_transaction_journal(
parent: &Path,
journal: &PrivateFileTransaction,
guard: &TrustedPathGuard,
) -> io::Result<()> {
let journal_path = parent.join(PRIVATE_FILE_TRANSACTION_JOURNAL);
if guarded_file_exists(guard, &journal_path).map_err(|error| {
with_context(
error,
format!(
"could not inspect private-file journal before publication: {}",
journal_path.display()
),
)
})? {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
"a private-file write transaction is already pending",
));
}
let bytes = serde_json::to_vec(journal)
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
let (staged, journal_file) =
stage_unique_bytes_retained(guard, parent, &bytes, "astrid-private-write-journal")?;
#[cfg(test)]
if let Err(error) = super::io::test_maybe_fail_journal_rename() {
drop(journal_file);
let _ = remove_guarded_file(guard, &staged);
return Err(error);
}
if let Err(error) = move_guarded_file(guard, &staged, &journal_path) {
drop(journal_file);
let _ = remove_guarded_file(guard, &staged);
return Err(error);View on GitHub (pinned to affd8760f4)
Solutions
- Retry the write: the library runs recover_private_file_transaction_locked on each call, which should roll back and remove a stale journal.
- Ensure only one process/thread writes private files in a given directory at a time (the transaction lock serializes them; check it was acquired).
- Inspect the directory for .astrid-private-write.transaction.json and .astrid-private.* files; if a stale journal persists after recovery, remove the leftover .astrid-private.* staging/rollback files and let a fresh call recover.
Example fix
// before
atomic_write_private_file(&path, data)?; // fails: AlreadyExists
// after: retry after recovery has cleared the pending journal
for attempt in 0..3 {
match atomic_write_private_file(&path, data) {
Ok(()) => break,
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists && attempt < 2 => continue,
Err(e) => return Err(e),
}
} Defensive patterns
Strategy: retry
Validate before calling
let journal_pending = dir.join(".astrid-private-write.transaction.json").exists();
if journal_pending { /* trigger a recovery pass (any read/write attempt runs recovery) before writing */ } Try / catch
match atomic_write_private_file(&path, data) {
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
// a transaction is pending; retry after recovery clears it
atomic_write_private_file(&path, data)?;
}
other => other?,
} Prevention
- Serialize private-file writes per directory across processes.
- Retry with backoff on AlreadyExists — recovery runs at the start of each call.
- Investigate crashes that leave journals behind.
When it happens
Trigger: A previous atomic_write_private_file crashed between journal publication and cleanup; two processes attempt private-file writes to the same directory concurrently without proper lock coordination; a stale journal survived a crash.
Common situations: App crash or power loss mid-write leaving the journal behind; parallel threads/processes writing different private files in the same directory; manually copied directories containing a leftover journal file.
Understand the failure class
Background: "already exists" / EEXIST / FileAlreadyExistsException: what the 'file already exists' error means and how to fix it — this error's family across 37 libraries.
Related errors
- AlreadyExists
- InvalidData
- an incomplete capsule authority update exists at {}; remove
- quarantined capsule authority bytes changed: {}
- leftover capsule authority receipt changed before retirement
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/a2df8c0118011e08.
Report an issue: GitHub.