dotnet/reactive · error · ArgumentNullException
Value cannot be null. (Parameter 'disposable')
Error message
Value cannot be null. (Parameter 'disposable')
What it means
SingleAssignmentAsyncDisposable.AssignAsync throws ArgumentNullException when the disposable passed is null. The class allows exactly one assignment (implemented via Interlocked.CompareExchange against null), and a null argument is rejected before any exchange is attempted.
Solutions
- Ensure a real IAsyncDisposable (or a no-op) is produced before assigning
- Guard at the call site: 'if (d != null) await single.AssignAsync(d);'
- Fix the factory returning null so it returns an empty/no-op disposable instead
- If the sink may or may not need a disposable, conditionally skip the AssignAsync call
Example fix
// before
await single.AssignAsync(_subscription); // _subscription is null on cancel path
// after
if (_subscription != null)
{
await single.AssignAsync(_subscription);
} Defensive patterns
Strategy: validation
Validate before calling
if (d is null)
throw new InvalidOperationException("Refusing to assign a null disposable.");
await single.AssignAsync(d); Type guard
bool CanAssign([NotNullWhen(true)] IAsyncDisposable? d) => d is not null;
Try / catch
try
{
await single.AssignAsync(d);
}
catch (ArgumentNullException ex) when (ex.ParamName == "disposable")
{
// subscription factory produced null; use a no-op disposable or skip assignment
} Prevention
- Replace nullable disposable fields with non-null defaults (no-op disposable)
- Null-check any factory output before handing it to AssignAsync
- Keep assignment paths free of conditional resource acquisition that can yield null
When it happens
Trigger: Calling AssignAsync(null) directly; callers listed in the region (firstTask/secondTask races, Append, Buffer, DoWhile sinks) passing a null disposable produced by an upstream factory or a not-yet-initialized field.
Common situations: A subscription/resource factory returns null on a failure or cancellation path; passing a nullable field that was never set; tests exercising the sink before wiring the real disposable.
Related errors
- Value cannot be null. (Parameter 'disposable')
- Value cannot be null. (Parameter 'disposable')
- keySelector
- Value cannot be null. (Parameter 'keySelector')
- Value cannot be null. (Parameter 'comparer')
AI-assisted analysis of dotnet/reactive@94b5d5ab91 (2026-09-15).
Data as JSON: /api/errors/8e5bdefacfbf7247.
Report an issue: GitHub.
Appendix: source
Thrown at AsyncRx.NET/System.Reactive.Async/Disposables/SingleAssignmentAsyncDisposable.cs:19
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT License.
// See the LICENSE file in the project root for more information.
using System.Threading;
using System.Threading.Tasks;
namespace System.Reactive.Disposables
{
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)