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

  1. Ensure a real IAsyncDisposable (or a no-op) is produced before assigning
  2. Guard at the call site: 'if (d != null) await single.AssignAsync(d);'
  3. Fix the factory returning null so it returns an empty/no-op disposable instead
  4. 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

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


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)