clockworklabs/SpacetimeDB · critical
should have a pending mutable anon tx as `procedure_start_mu
Error message
should have a pending mutable anon tx as `procedure_start_mut_tx` preceded
What it means
try_with_tx runs a procedure body inside an anonymous mutable transaction: procedure_start_mut_tx begins it and procedure_abort_mut_tx rolls it back on failure. This .expect fires on the abort path when the host answers that no transaction is pending - bindings and host disagree about transaction state (the start never happened or the transaction already ended). It is an internal invariant/ABI violation, not an application-level condition.
Source
Thrown at crates/bindings/src/lib.rs:1200
impl Deref for TxContext {
type Target = ReducerContext;
fn deref(&self) -> &Self::Target {
&self.0
}
}
/// 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);View on GitHub (pinned to 524b4487d9)
Solutions
- Rebuild and redeploy the module with bindings that match the running host version.
- Never call the procedure transaction syscalls directly; use the provided context wrappers only.
- If versions match, report the bug with both versions and a minimal reproduction.
Defensive patterns
Strategy: try-catch
Try / catch
// Panic, not Err: isolate at the host/embedding boundary.
let outcome = std::panic::catch_unwind(std::AssertUnwindSafe(|| invoke_procedure()));
if outcome.is_err() {
// ABI state is now suspect: log versions of module and host, restart the
// module instance, and redeploy with matched bindings.
} Prevention
- Build modules with the same bindings version as the host node.
- Never call procedure_start_mut_tx / procedure_abort_mut_tx manually.
- Pin versions in CI so host and guest artifacts upgrade together.
When it happens
Trigger: Guest bindings built against a host with a different procedure-tx ABI; invoking procedure_start_mut_tx / procedure_abort_mut_tx manually so the start/abort pairing breaks; a host error path that already finalized the transaction before the bindings abort it.
Common situations: Mixing module and node versions after a partial upgrade; hand-rolled syscall wrappers in tests; a regression in a bindings release.
Related errors
- holding `&mut HandlerContext`, so should not be in a tx alre
- transaction retry failed again
- Invalid header name from host
- Failed to parse JWT payload
- never types are not yet supported in C# output
AI-assisted analysis of clockworklabs/SpacetimeDB@524b4487d9 (2026-08-16).
Data as JSON: /api/errors/facf955c4568f13a.
Report an issue: GitHub.