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

  1. Retry the write: the library runs recover_private_file_transaction_locked on each call, which should roll back and remove a stale journal.
  2. Ensure only one process/thread writes private files in a given directory at a time (the transaction lock serializes them; check it was acquired).
  3. 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

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


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/a2df8c0118011e08. Report an issue: GitHub.