dotnet/reactive · error · InvalidOperationException
Disposable already assigned.
Error message
Disposable already assigned.
What it means
SingleAssignmentAsyncDisposable.AssignAsync throws InvalidOperationException("Disposable already assigned.") when AssignAsync is called a second time while the disposable is neither unset nor disposed. The CompareExchange observes a non-null old value that is not the Disposed sentinel, meaning a second disposable is being attached to a single-assignment slot — a contract violation the library fails fast on.
Solutions
- Create a new SingleAssignmentAsyncDisposable per subscription/assignment instead of reusing one instance
- Serialize the assignment (lock, semaphore, or assign-once flag) so only the first caller assigns
- Check whether assignment already happened before calling AssignAsync and skip the second call
- If re-assignment is intentional, dispose the old single-assignment disposable first (after disposal, AssignAsync disposes the new one without throwing) or use a different container (e.g. SerialAsyncDisposable) that supports replacement
Example fix
// before
await sink.Disposable.AssignAsync(d1);
await sink.Disposable.AssignAsync(d2); // throws: already assigned
// after
await sink.Disposable.AssignAsync(d1);
if (!assigned)
{
assigned = true;
await sink.Disposable.AssignAsync(d2);
}
// or, when replacement is intended, use SerialAsyncDisposable:
await serial.AssignAsync(d2); // replaces d1 safely Defensive patterns
Strategy: validation
Validate before calling
// allow assignment only once per slot, or after disposal
if (Interlocked.Exchange(ref assignedFlag, 1) == 0)
{
await single.AssignAsync(d);
}
else
{
await d.DisposeAsync(); // dispose the surplus disposable instead of throwing
} Type guard
bool CanAssignNow() => assignedFlag == 0;
Try / catch
try
{
await single.AssignAsync(d);
}
catch (InvalidOperationException ex) when (ex.Message == "Disposable already assigned.")
{
// double assignment: dispose the redundant disposable and keep the first one
await d.DisposeAsync();
} Prevention
- Create a fresh SingleAssignmentAsyncDisposable per subscription instead of sharing instances
- Synchronize concurrent assignment paths (task race in firstTask/secondTask patterns) so only one assigns
- Use SerialAsyncDisposable when replacement (rather than single assignment) is the intent
- Track assignment state with an explicit flag or check the current disposable before assigning
When it happens
Trigger: Calling AssignAsync twice on the same instance; races between concurrent tasks (firstTask/secondTask patterns) that both assign; operator sinks (Append, Buffer, DoWhile) re-subscribing or re-entering the assignment path after the single-assignment disposable was already populated and not yet disposed.
Common situations: Re-running a query builder that shares one SingleAssignmentAsyncDisposable across subscriptions; a race where two concurrent async flows both try to register their cleanup; forgetting to create a fresh sink per subscription in a cold observable.
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
- Value cannot be null. (Parameter 'disposable')
- Value cannot be null. (Parameter 'disposable')
- Could not find static event '{0}' on type '{1}'.
- Could not find instance event '{0}' on type '{1}'.
- Missing add method on event.
AI-assisted analysis of dotnet/reactive@94b5d5ab91 (2026-09-15).
Data as JSON: /api/errors/5a1e022b88fea59a.
Report an issue: GitHub.
Appendix: source
Thrown at AsyncRx.NET/System.Reactive.Async/Disposables/SingleAssignmentAsyncDisposable.cs:27
{
public sealed class SingleAssignmentAsyncDisposable : IAsyncDisposable
{
private static readonly IAsyncDisposable Disposed = AsyncDisposable.Create(() => default);
private IAsyncDisposable _disposable;
public async ValueTask AssignAsync(IAsyncDisposable disposable)
{
if (disposable == null)
throw new ArgumentNullException(nameof(disposable));
var old = Interlocked.CompareExchange(ref _disposable, disposable, null);
if (old == null)
return;
if (old != Disposed)
throw new InvalidOperationException("Disposable already assigned.");
await disposable.DisposeAsync().ConfigureAwait(false);
}
public ValueTask DisposeAsync()
{
return Interlocked.Exchange(ref _disposable, Disposed)?.DisposeAsync() ?? default;
}
}
}
View on GitHub (pinned to 94b5d5ab91)