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

  1. Rebuild and redeploy the module with bindings that match the running host version.
  2. Never call the procedure transaction syscalls directly; use the provided context wrappers only.
  3. 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

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


AI-assisted analysis of clockworklabs/SpacetimeDB@524b4487d9 (2026-08-16). Data as JSON: /api/errors/facf955c4568f13a. Report an issue: GitHub.