stride3d/stride · error · TransactionException

This transaction has already been completed.

Error message

This transaction has already been completed.

What it means

Thrown by Transaction.Complete when the transaction's referenceCount has already reached zero, i.e. the transaction was already completed. Complete decrements the reference count and finishes the transaction when the last reference is released.

Solutions

  1. Call Complete() exactly once per transaction; do not also call it from Dispose paths you control
  2. Track completion with a bool flag before calling Complete again
  3. If sharing, have a single owner responsible for completing the transaction

Example fix

// before
using var transaction = stack.CreateTransaction();
transaction.Complete(); // then Dispose path may complete again
// after
var transaction = stack.CreateTransaction();
try { /* edits */ }
finally { transaction.Dispose(); } // single completion path
Defensive patterns

Strategy: try-catch

Validate before calling

if (completed) return;
completed = true;
transaction.Complete();

Type guard

bool CanComplete(Transaction t) => !completedFlags.GetValueOrDefault(t, false);

Try / catch

try { transaction.Complete(); }
catch (TransactionException ex) when (ex.Message == "This transaction has already been completed.")
{
    // already completed elsewhere; treat as no-op
}

Prevention

When it happens

Trigger: Calling Transaction.Complete() twice on the same transaction — commonly calling both Dispose() and Complete(), or keeping shared references and completing from multiple code paths.

Common situations: Using statements combined with explicit Complete() calls, transaction objects shared between services that each call Complete.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of stride3d/stride@96fad776d2 (2026-09-14). Data as JSON: /api/errors/40cb9902db011794. Report an issue: GitHub.

Appendix: source

Thrown at sources/core/Stride.Core.Design/Transactions/Transaction.cs:64

    }

    /// <inheritdoc/>
    public void Continue()
    {
        synchronizationContext = SynchronizationContext.Current;
    }

    /// <inheritdoc/>
    public void AddReference()
    {
        referenceCount++;
    }

    /// <inheritdoc/>
    public void Complete()
    {
        if (referenceCount == 0)
            throw new TransactionException("This transaction has already been completed.");

        // Transaction might be kept alive by others, only process it if last reference
        // Note: this KeepAlive() and Complete() are not thread-safe, no need to use interlocked
        if (referenceCount == 1)
        {
            // Disabling synchronization context check: when we await for dispatcher task we always resume in a different SC so it makes it difficult to enforce this rule.
            //if (synchronizationContext != SynchronizationContext.Current)
            //    throw new TransactionException("This transaction is being completed in a different synchronization context.");

            TryMergeOperations();
            transactionStack.CompleteTransaction(this);
            // Don't keep reference to synchronization context after completion
            synchronizationContext = null;
        }

        --referenceCount;
    }

View on GitHub (pinned to 96fad776d2)