dotnet/reactive · error · ObjectDisposedException
RefCountDisposable
Error message
RefCountDisposable
What it means
GetDisposable throws ObjectDisposedException named 'RefCountDisposable' when the RefCountDisposable was constructed with throwWhenDisposed:true, is already disposed, and the active reference count is zero. Because bit 31 of the packed count marks disposal, the code detects 'disposed and no children' and refuses to hand out inner disposables.
Solutions
- Check the IsDisposed property before calling GetDisposable and take an alternative path.
- Construct with throwWhenDisposed:false if post-dispose GetDisposable calls should return Disposable.Empty instead of throwing.
- Fix lifecycle ordering so GetDisposable is only called while the parent is alive (e.g. within a using block or while holding a lease).
Example fix
// before
var d = refCount.GetDisposable(); // may throw after dispose
// after
if (!refCount.IsDisposed)
{
var d = refCount.GetDisposable();
} Defensive patterns
Strategy: try-catch
Validate before calling
if (refCount.IsDisposed) { /* take alternate path */ } else { var d = refCount.GetDisposable(); } Type guard
static bool CanGetDisposable(RefCountDisposable rc) => !rc.IsDisposed;
Try / catch
try { var d = refCount.GetDisposable(); }
catch (ObjectDisposedException) { /* treat as Disposable.Empty or re-acquire a new RefCountDisposable */ } Prevention
- Check IsDisposed before every GetDisposable call on shared instances.
- Prefer constructing with throwWhenDisposed:false when post-dispose calls are expected and benign.
- Scope GetDisposable usage inside using blocks tied to the parent's lifetime.
When it happens
Trigger: Calling GetDisposable() on a RefCountDisposable created with new RefCountDisposable(disposable, throwWhenDisposed:true) after Dispose() has been called and all outstanding inner disposables have been released.
Common situations: Code that keeps a long-lived RefCountDisposable, disposes it on shutdown, and then accidentally calls GetDisposable again from a race or a stale code path; also tests intentionally verifying the throw-on-disposed behavior.
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
- EventLoopScheduler
- Value cannot be null. (Parameter 'disposable')
- RefCountDisposable can't handle more than 2147483647…
- Value cannot be null. (Parameter 'disposable')
- Strings_Core.DISPOSABLE_ALREADY_ASSIGNED
AI-assisted analysis of dotnet/reactive@94b5d5ab91 (2026-09-15).
Data as JSON: /api/errors/8106c1bda52c68a1.
Report an issue: GitHub.
Appendix: source
Thrown at Rx.NET/Source/src/System.Reactive/Disposables/RefCountDisposable.cs:69
/// <summary>
/// Returns a dependent disposable that when disposed decreases the refcount on the underlying disposable.
/// </summary>
/// <returns>A dependent disposable contributing to the reference count that manages the underlying disposable's lifetime.</returns>
/// <exception cref="ObjectDisposedException">This instance has been disposed and is configured to throw in this case by <see cref="RefCountDisposable(IDisposable, bool)"/>.</exception>
public IDisposable GetDisposable()
{
// the current state
var cnt = Volatile.Read(ref _count);
for (; ; )
{
// If bit 31 is set and the active count is zero, don't create an inner
if (cnt == int.MinValue)
{
if (_throwWhenDisposed)
{
throw new ObjectDisposedException("RefCountDisposable");
}
return Disposable.Empty;
}
// Should not overflow the bits 0..30
if ((cnt & 0x7FFFFFFF) == int.MaxValue)
{
throw new OverflowException($"RefCountDisposable can't handle more than {int.MaxValue} disposables");
}
// Increment the active count by one, works because the increment
// won't affect bit 31
var u = Interlocked.CompareExchange(ref _count, cnt + 1, cnt);
if (u == cnt)
{
return new InnerDisposable(this);
}View on GitHub (pinned to 94b5d5ab91)