clockworklabs/SpacetimeDB · critical
holding `&mut HandlerContext`, so should not be in a tx alre
Error message
holding `&mut HandlerContext`, so should not be in a tx already; called manually elsewhere?
What it means
procedure_start_mut_tx failed because a transaction is already active for this call - try_with_tx was entered re-entrantly (a transactional procedure invoked while another transaction from the same context is still open). The .expect converts the host's refusal into a panic.
Source
Thrown at crates/bindings/src/lib.rs:1205
}
}
/// We need to passthrough identity and connection_id because procedures can be invoked by users.
/// For [HttpContext] this is always anonymous ([Identity::ZERO]).
/// Construct the inner [ReducerContext] with the appropriate caller information.
fn try_with_tx<T, E>(
body: impl Fn(&TxContext) -> Result<T, E>,
identity: Identity,
connection_id: Option<ConnectionId>,
) -> Result<T, E> {
let abort = || {
crate::sys::procedure::procedure_abort_mut_tx()
.expect("should have a pending mutable anon tx as `procedure_start_mut_tx` preceded")
};
let run = || {
let timestamp = crate::sys::procedure::procedure_start_mut_tx()
.expect("holding `&mut HandlerContext`, so should not be in a tx already; called manually elsewhere?");
let timestamp = Timestamp::from_micros_since_unix_epoch(timestamp);
let tx = ReducerContext::new(crate::Local {}, identity, connection_id, timestamp);
let tx = TxContext(tx);
struct DoOnDrop<F: Fn()>(F);
impl<F: Fn()> Drop for DoOnDrop<F> {
fn drop(&mut self) {
(self.0)();
}
}
let abort_guard = DoOnDrop(abort);
let res = body(&tx);
core::mem::forget(abort_guard);
res
};
let mut res = run();View on GitHub (pinned to 524b4487d9)
Solutions
- Restructure so transaction-scoped helpers are not nested; perform side effects after the outer transaction completes.
- Remove manual syscall usage that opens a transaction before the wrapper runs.
- Align module bindings with the host version.
Defensive patterns
Strategy: try-catch
Try / catch
let outcome = std::panic::catch_unwind(std::AssertUnwindSafe(|| invoke_procedure()));
if outcome.is_err() {
// Nested-transaction attempt: log the call stack, restructure so tx-scoped
// helpers are not invoked re-entrantly.
} Prevention
- Do not invoke transactional procedures/helpers from inside another open transaction.
- Perform HTTP or scheduling side effects after the outer transaction commits.
- Remove any manual procedure tx syscalls from module code.
When it happens
Trigger: Invoking a tx-wrapped procedure or helper from inside another reducer/procedure that already holds the anonymous mutable tx; calling procedure_start_mut_tx manually before a library wrapper runs; version skew between bindings and host.
Common situations: Refactoring so transactional helpers end up nested; scheduling side-effecting calls inside an open transaction; mixing old and new bindings.
Related errors
- should have a pending mutable anon tx as `procedure_start_mu
- Invalid header name from host
- transaction retry failed again
- invalid transaction offset {}, expected {}
- Failed to parse JWT payload
AI-assisted analysis of clockworklabs/SpacetimeDB@524b4487d9 (2026-08-16).
Data as JSON: /api/errors/a5ce5134304e4214.
Report an issue: GitHub.