clockworklabs/SpacetimeDB · error · InvalidOperationException

Transaction context was not initialised.

Error message

Transaction context was not initialised.

What it means

The module runtime keeps per-invocation transaction state (TransactionalContextState): EnterTxContext opens it (called when the host starts a reducer/procedure transaction) and ExitTxContext clears it. RequireTxContext is what generated table/DbContext accessors call to obtain the user-facing transaction context; if txContext is null — i.e. no transaction is active on this thread — it throws this InvalidOperationException. It means module code touched the database outside a host-driven transaction.

Source

Thrown at crates/bindings-csharp/Runtime/TransactionalContextState.cs:51

    private Internal.TxContext? txContext;
    private TTxContext? cachedUserTxContext;

    public Internal.TxContext EnterTxContext(long timestampMicros)
    {
        var timestamp = new Timestamp(timestampMicros);
        Timestamp = timestamp;
        txContext = txContext?.WithTimestamp(timestamp) ?? createInitialTxContext(timestamp);
        return txContext;
    }

    public void ExitTxContext() => txContext = null;

    public TTxContext RequireTxContext()
    {
        var inner =
            txContext
            ?? throw new InvalidOperationException("Transaction context was not initialised.");
        cachedUserTxContext ??= createTxContext(inner);
        cachedUserTxContext.Refresh(inner);
        return cachedUserTxContext;
    }

    public TResult WithTx<TResult>(Func<TTxContext, TResult> body) =>
        TryWithTx(tx => Result<TResult, Exception>.Ok(body(tx))).UnwrapOrThrow();

    public TxOutcomeCore<TResult> TryWithTx<TResult, TError>(
        Func<TTxContext, Result<TResult, TError>> body
    )
        where TError : Exception
    {
        try
        {
            var result = RunWithRetry(body);

            return result switch

View on GitHub (pinned to 6dee26c6ef)

Solutions

  1. Move all table access inside [Reducer]/[Procedure] methods so the host enters the transaction first
  2. For HTTP handlers and other non-transactional entry points, wrap DB work in WithTx(...) / TryWithTx(...) which manage EnterTxContext/ExitTxContext
  3. Delete seeding/static-init code that touches tables and replace it with an init reducer the host invokes
  4. Never cache or share Db handles across invocations — always resolve them inside the transactional callback

Example fix

// before - table touched outside any transaction
public static class Module {
    static Module() { MyTable.Insert(new Row(1)); } // RequireTxContext throws
}

// after - seed from a lifecycle reducer (or wrap in WithTx)
[SpacetimeDB.Reducer]
public static void Init(ReducerContext ctx) { MyTable.Insert(new Row(1)); }
Defensive patterns

Strategy: validation

Try / catch

try { DoTableWork(); }
catch (InvalidOperationException ioe) when (ioe.Message == "Transaction context was not initialised.")
{
    // code reached DB access outside a host transaction; move it into a reducer/procedure
    // or wrap with WithTx/TryWithTx rather than swallowing it
}

Prevention

When it happens

Trigger: Calling table accessors (generated Db handles, Iter/Insert/Delete) from module static constructors, static field initializers, or arbitrary methods invoked outside a reducer/procedure callback; running DB logic in an HTTP handler body without going through WithTx/TryWithTx; calling reducer helper code from a background thread or timer.

Common situations: Seeding data at module startup from a constructor instead of a lifecycle reducer; refactoring reducer bodies into shared helpers that are then also called from non-transactional contexts; new HTTP handler features that read tables directly; unit tests invoking generated Db methods without the host harness.

Related errors


AI-assisted analysis of clockworklabs/SpacetimeDB@6dee26c6ef (2026-08-20). Data as JSON: /api/errors/dae08c0bba85b881. Report an issue: GitHub.