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

  1. Restructure so transaction-scoped helpers are not nested; perform side effects after the outer transaction completes.
  2. Remove manual syscall usage that opens a transaction before the wrapper runs.
  3. 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

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


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