stride3d/stride · error · InvalidOperationException

Cannot release an object that doesn't have active…

Error message

Cannot release an object that doesn't have active reference. AddReference/Release pair must match.

What it means

Stride's ReferenceBase implements manual reference counting; Release() decrements the counter returned by AddReference()/IncrementReference. Throwing here means the internal counter went below zero, i.e. Release was called more times than AddReference (or on an object with no active references), which would corrupt lifetime management and cause premature destruction or leaks.

Solutions

  1. Ensure every Release() call has exactly one matching AddReference()/IncrementReference() call on the same object instance
  2. Audit ownership: only the component that called AddReference should Release; wrap Add/Release in RAII-style helpers or using blocks
  3. Guard against double-dispose with a bool flag or by checking the object's disposed state before calling Release
  4. If a destroy method throws during Release, fix the exception source; the rollback path leaves the counter incremented and the object alive
  5. Use a memory profiler or debug counter logging to find the unbalanced Add/Release pair

Example fix

// before
entity.Release();
entity.Release(); // counter underflows -> InvalidOperationException
// after
entity.AddReference();
try { Use(entity); } finally { entity.Release(); } // exactly one release per reference
Defensive patterns

Strategy: try-catch

Validate before calling

// track references yourself alongside the API
private readonly HashSet<object> heldRefs = new();
bool CanRelease(object o) => heldRefs.Contains(o);

Type guard

bool HasActiveReference(object o) => o is ReferenceBase rb && heldRefs.Contains(rb);

Try / catch

try
{
    referenceBase.Release();
}
catch (InvalidOperationException ex)
{
    logger.Warn(ex, "Unbalanced Release: no active reference for {Type}", referenceBase.GetType());
}

Prevention

When it happens

Trigger: Calling Release() (or Dispose path that releases) on an object without a prior matching AddReference(); calling Release() twice for one reference; releasing an object owned by another subsystem; calling Release() after the counter was restored by the exception-rollback path (Interlocked.Exchange back to newCounter+1) when a destroy method threw.

Common situations: Double-dispose in user code (e.g. object disposed by both a using block and a container); sharing an entity/component between two systems that each call Release; a destroy callback throwing inside Release leaves the counter partially rolled back and a later Release then underflows; porting code that assumed GC semantics.

Related errors


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

Appendix: source

Thrown at sources/core/Stride.Core/ReferenceBase.cs:41

    /// <inheritdoc/>
    public virtual int Release()
    {
        var newCounter = Interlocked.Decrement(ref counter);
        if (newCounter == 0)
        {
            try
            {
                Destroy();
            }
            finally
            {
                // Reverse back the counter if there are any exceptions in the destroy method
                Interlocked.Exchange(ref counter, newCounter + 1);
            }
        }
        else if (newCounter < 0)
        {
            throw new InvalidOperationException(FrameworkResources.ReleaseReferenceError);
        }
        return newCounter;
    }

    /// <summary>
    /// Releases unmanaged and - optionally - managed resources
    /// </summary>
    protected abstract void Destroy();
}

View on GitHub (pinned to 96fad776d2)