facebook/flow · error

an active transaction may only be committed once

Error message

an active transaction may only be committed once

What it means

ActiveTransaction::commit calls self.0.take(), which empties the Option holding the transaction; the expect panics if it is already None. This means commit() (or Drop, which also consumes the transaction) was invoked twice on the same ActiveTransaction — an API misuse, since committing is a one-shot, move-consuming operation. The library enforces the single-commit invariant with this panic.

Source

Thrown at rust_port/crates/flow_heap/src/transaction.rs:113

            heap,
            overlay: HeapOverlay::new(),
            committed: RwLock::new(Some(guard)),
        })))
    }

    pub fn handle(&self) -> Arc<Transaction> {
        self.0
            .as_ref()
            .expect("an active transaction cannot be used after commit")
            .dupe()
    }

    /// Publishes the overlay into the heap the transaction was opened on.
    pub fn commit(mut self) {
        let transaction = self
            .0
            .take()
            .expect("an active transaction may only be committed once");
        let destination = transaction.committed_heap();
        transaction.commit(&destination);
    }
}

impl Drop for ActiveTransaction {
    fn drop(&mut self) {
        if let Some(transaction) = self.0.as_ref() {
            transaction.release();
        }
    }
}

/// Borrows the committed heap for the duration of one read. Produced by
/// [`Transaction::latest_reader`] / [`Transaction::committed_reader`].
pub struct HeapAccess<'a> {
    state: CommittedStateAccess<'a>,
    overlay: Option<&'a HeapOverlay>,

View on GitHub (pinned to 5c86586199)

Solutions

  1. Commit each ActiveTransaction exactly once: consume it by value (tx.commit()) and do not keep or re-create a handle afterwards.
  2. Restructure code so the guard is moved into a single code path; use the returned/dropped state to get a fresh transaction if needed (drop rolls back and the next get returns a fresh overlay).
  3. Use Option::take yourself or check is_some() before a second logical commit, converting the panic into a controlled error.
  4. Avoid wrappers with interior mutability around ActiveTransaction; rely on Rust's move semantics to make double commit unrepresentable.

Example fix

// before
let tx = heap.transaction();
tx.commit();
tx.commit(); // panics: transaction already consumed
// after
let tx = heap.transaction();
tx.commit(); // exactly once; open a new transaction for more work
let tx2 = heap.transaction();
tx2.commit();
Defensive patterns

Strategy: type-guard

Validate before calling

fn can_commit(tx: &Option<ActiveTransaction>) -> bool {
    tx.is_some() // only commit when a transaction handle is present
}

Type guard

fn is_active(tx: &ActiveTransaction) -> bool { !tx.is_consumed() } // or track via Option<ActiveTransaction> and check is_some()

Try / catch

let result = std::panic::catch_unwind(|| { tx.commit(); });
if result.is_err() {
    eprintln!("transaction already committed; opening a fresh one");
    let tx = heap.transaction();
    tx.commit();
}

Prevention

When it happens

Trigger: Calling .commit() on the same ActiveTransaction value twice (only possible if a copy/clone of the guard was retained, or commit is called after the guard was already consumed or dropped); tests like commit_rejects_retained_transaction_handles exercise exactly this retained-handle case.

Common situations: Storing the ActiveTransaction in two places (e.g. cloning a wrapper that shares the Option via interior mutability such as Rc<RefCell<>>); calling commit() inside a helper and again at the call site; calling commit() after an early-return path already dropped the guard.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of facebook/flow@5c86586199 (2026-09-08). Data as JSON: /api/errors/4ff68e0de9512d4b. Report an issue: GitHub.