dotnet/reactive · error · ArgumentNullException

subscriptions

Error message

subscriptions

What it means

AsyncObserver.GroupJoin throws ArgumentNullException when the subscriptions composite disposable is null. The subscriptions container tracks all duration-observable subscriptions so they can be disposed when either source completes; a null value would leak or crash the join logic. This is an internal invariant enforced by a fail-fast guard.

Solutions

  1. Construct a CompositeAsyncDisposable and pass it as the subscriptions argument.
  2. Follow the pattern in GroupJoin.cs: create `var subscriptions = new CompositeAsyncDisposable();` before calling AsyncObserver.GroupJoin.
  3. If wrapping, never forward a null disposable — throw your own descriptive exception earlier.

Example fix

// before
var (l, r, d) = AsyncObserver.GroupJoin(observer, null, ld, rd, rs);
// after
var subscriptions = new CompositeAsyncDisposable();
var (l, r, d) = AsyncObserver.GroupJoin(observer, subscriptions, ld, rd, rs);
Defensive patterns

Strategy: validation

Validate before calling

if (subscriptions == null) subscriptions = new CompositeAsyncDisposable();

Type guard

bool HasSubscriptions(IAsyncDisposable d) => d is not null;

Try / catch

try { var parts = AsyncObserver.GroupJoin(observer, subs, ld, rd, rs); }
catch (ArgumentNullException ex) when (ex.ParamName == "subscriptions") { /* construct CompositeAsyncDisposable and retry */ }

Prevention

When it happens

Trigger: Calling AsyncObserver.GroupJoin with null as the second (IAsyncDisposable subscriptions) argument — typically from custom operator code that forgot to construct the CompositeAsyncDisposable before delegating.

Common situations: Hand-rolled GroupJoin-like operators; unit tests calling the factory directly; code refactors where disposable creation moved after the factory call.

Related errors


AI-assisted analysis of dotnet/reactive@94b5d5ab91 (2026-09-15). Data as JSON: /api/errors/b9ef3474540633e5. Report an issue: GitHub.

Appendix: source

Thrown at AsyncRx.NET/System.Reactive.Async/Linq/Operators/GroupJoin.cs:52

                var leftSubscription = await left.SubscribeSafeAsync(leftObserver).ConfigureAwait(false);
                await subscriptions.AddAsync(leftSubscription).ConfigureAwait(false);

                var rightSubscription = await right.SubscribeSafeAsync(rightObserver).ConfigureAwait(false);
                await subscriptions.AddAsync(rightSubscription).ConfigureAwait(false);

                return disposable;
            });
        }
    }

    public partial class AsyncObserver
    {
        public static (IAsyncObserver<TLeft>, IAsyncObserver<TRight>, IAsyncDisposable) GroupJoin<TLeft, TRight, TLeftDuration, TRightDuration, TResult>(IAsyncObserver<TResult> observer, IAsyncDisposable subscriptions, Func<TLeft, IAsyncObservable<TLeftDuration>> leftDurationSelector, Func<TRight, IAsyncObservable<TRightDuration>> rightDurationSelector, Func<TLeft, IAsyncObservable<TRight>, TResult> resultSelector)
        {
            if (observer == null)
                throw new ArgumentNullException(nameof(observer));
            if (subscriptions == null)
                throw new ArgumentNullException(nameof(subscriptions));
            if (leftDurationSelector == null)
                throw new ArgumentNullException(nameof(leftDurationSelector));
            if (rightDurationSelector == null)
                throw new ArgumentNullException(nameof(rightDurationSelector));
            if (resultSelector == null)
                throw new ArgumentNullException(nameof(resultSelector));

            var gate = new AsyncGate();

            var group = new CompositeAsyncDisposable(subscriptions);
            var refCount = new RefCountAsyncDisposable(group);

            var leftMap = new SortedDictionary<int, IAsyncObserver<TRight>>();
            var rightMap = new SortedDictionary<int, TRight>();

            var leftId = default(int);
            var rightId = default(int);

View on GitHub (pinned to 94b5d5ab91)