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

Release() decrements the reference counter atomically; if the resulting counter is negative, the caller has released more times than it acquired. The library throws InvalidOperationException to flag the unbalanced AddReference/Release pair before the corrupted counter causes worse failures. Every Release must correspond to exactly one prior AddReference.

Solutions

  1. Find the extra Release call and remove it, or guard it so it only runs when that code path actually owns a reference
  2. Track reference ownership explicitly: only call Release on objects your component AddReference'd itself
  3. Check whether Dispose/DisposeMember is also releasing, causing a double decrement, and restructure ownership
  4. Add logging in OnRelease overrides to trace the count per call site during development

Example fix

// before
finally
{
    texture.Release(); // may run after texture was already released on the error path above
}
// after
finally
{
    if (!released)
    {
        texture.Release();
        released = true;
    }
}
Defensive patterns

Strategy: validation

Validate before calling

if (obj is IReferencable r && r.RefCount > 0)
{
    r.Release();
}

Type guard

static bool OwnsReference(DisposeBase obj) =>
    obj is IReferencable && obj.RefCount > 0 && !obj.IsDisposed;

Try / catch

try
{
    ((IReferencable)obj).Release();
}
catch (InvalidOperationException ex) when (ex.Message.Contains("doesn't have active reference"))
{
    // unbalanced Release: log and skip; do not retry the release
}

Prevention

When it happens

Trigger: Calling IReferencable.Release() more times than AddReference() was called on the same object — e.g. an extra Release in an error/cleanup path, double-disposal, or calling Release on a never-referenced object.

Common situations: Cleanup code that releases unconditionally in a finally block after an earlier release already ran; wrapping code that both disposes and releases; sharing objects between systems where one side also releases.

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/d9f4f831915d420d. Report an issue: GitHub.

Appendix: source

Thrown at sources/core/Stride.Core/DisposeBase.cs:87

            throw new InvalidOperationException(FrameworkResources.AddReferenceError);

        return newCounter;
    }

    /// <inheritdoc/>
    int IReferencable.Release()
    {
        OnReleaseReference();

        var newCounter = Interlocked.Decrement(ref refCount);
        if (newCounter == 0)
        {
            Destroy();
            IsDisposed = true;
        }
        else if (newCounter < 0)
        {
            throw new InvalidOperationException(FrameworkResources.ReleaseReferenceError);
        }
        return newCounter;
    }

    /// <summary>
    ///   Called when a new reference of this object has been counted (via a call to <see cref="IReferencable.AddReference"/>).
    /// </summary>
    protected virtual void OnAddReference() { }

    /// <summary>
    ///   Called when a call to <see cref="IReferencable.Release"/> has decremented the reference count of this object.
    /// </summary>
    protected virtual void OnReleaseReference() { }
}

View on GitHub (pinned to 96fad776d2)