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
- Commit each ActiveTransaction exactly once: consume it by value (tx.commit()) and do not keep or re-create a handle afterwards.
- 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).
- Use Option::take yourself or check is_some() before a second logical commit, converting the panic into a controlled error.
- 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
- Model the transaction as Option<ActiveTransaction> and take() it before committing.
- Never store transaction guards in cloneable/shared containers (Rc, RefCell).
- Remember Drop performs an implicit rollback: after any drop, open a new transaction.
- Keep commit calls in one clearly-owned code path per transaction.
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
- Unknown exception reading from the server: {}
- Error sending command to server: {}
- invalid line
- invalid column
- failed to write cli errors
AI-assisted analysis of facebook/flow@5c86586199 (2026-09-08).
Data as JSON: /api/errors/4ff68e0de9512d4b.
Report an issue: GitHub.