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
- Ensure every Release() call has exactly one matching AddReference()/IncrementReference() call on the same object instance
- Audit ownership: only the component that called AddReference should Release; wrap Add/Release in RAII-style helpers or using blocks
- Guard against double-dispose with a bool flag or by checking the object's disposed state before calling Release
- If a destroy method throws during Release, fix the exception source; the rollback path leaves the counter incremented and the object alive
- 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
- Pair every AddReference with exactly one Release in the same scope
- Use try/finally so exceptions never skip the Release (or the Add) side
- Track ownership in a container instead of ad-hoc Release calls
- Never Release objects you did not AddReference yourself
- Log Add/Release calls in debug builds to spot imbalances early
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
- Cannot add a reference for an object already released…
- Custom strides is not supported with packed PixelFormats
- Unable to find the base
- Unable to find the graph corresponding to the base part
- An IObjectNode was expected when processing the path
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)