dotnet/reactive · error · ArgumentNullException

comparer

Error message

comparer

What it means

The comparer overload of GroupByUntil requires an IEqualityComparer<TKey> to group keys; unlike some other overloads there is no default-comparer fallback here, so a null comparer throws ArgumentNullException with parameter name "comparer" (message "Value cannot be null. (Parameter 'comparer')") during eager validation. The exception is thrown synchronously at the call, before any subscription begins.

Solutions

  1. Pass a valid comparer instance, e.g. `EqualityComparer<TKey>.Default`, or your custom IEqualityComparer<TKey>.
  2. If you do not need custom key equality, use the two/three-argument overloads of GroupByUntil that omit the comparer so defaults apply.
  3. Ensure DI/configuration actually provides the comparer; fail at startup if the registration is missing.
  4. Catch ArgumentNullException at the call site if a missing comparer is an expected, recoverable condition.

Example fix

// before
var grouped = source.GroupByUntil(x => x.Key, g => g.TakeUntil(done), null);
// after
var grouped = source.GroupByUntil(x => x.Key, g => g.TakeUntil(done), EqualityComparer<string>.Default);
Defensive patterns

Strategy: validation

Validate before calling

if (comparer is null)
    comparer = EqualityComparer<TKey>.Default; // or throw explicitly

Type guard

static bool HasComparer<TKey>(IEqualityComparer<TKey>? c) => c is not null;

Try / catch

try
{
    var grouped = source.GroupByUntil(keySelector, durationSelector, comparer);
}
catch (ArgumentNullException ex) when (ex.ParamName == "comparer")
{
    // fall back to EqualityComparer<TKey>.Default
}

Prevention

When it happens

Trigger: Calling `source.GroupByUntil(keySelector, durationSelector, null)` where the comparer variable is null — e.g. an injected IEqualityComparer<TKey> not registered in DI, a custom comparer returned null from a factory, or a typo passing null where a comparer instance was intended.

Common situations: DI containers missing an IEqualityComparer<TKey> registration; comparer instances conditionally initialized; refactoring where a default comparer overload call was switched to the comparer overload without supplying an instance.

Related errors


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

Appendix: source

Thrown at AsyncRx.NET/System.Reactive.Async/Linq/Operators/GroupByUntil.cs:184

            if (durationSelector == null)
                throw new ArgumentNullException(nameof(durationSelector));

            return CreateAsyncObservable<IGroupedAsyncObservable<TKey, TSource>>.From(
                source,
                (keySelector, durationSelector),
                static (source, state, observer) => GroupByUntilCore<TSource, TKey, TSource, TDuration>(source, observer, (o, d) => AsyncObserver.GroupByUntil(o, d, state.keySelector, state.durationSelector)));
        }

        public static IAsyncObservable<IGroupedAsyncObservable<TKey, TSource>> GroupByUntil<TSource, TKey, TDuration>(this IAsyncObservable<TSource> source, Func<TSource, ValueTask<TKey>> keySelector, Func<IGroupedAsyncObservable<TKey, TSource>, IAsyncObservable<TDuration>> durationSelector, IEqualityComparer<TKey> comparer)
        {
            if (source == null)
                throw new ArgumentNullException(nameof(source));
            if (keySelector == null)
                throw new ArgumentNullException(nameof(keySelector));
            if (durationSelector == null)
                throw new ArgumentNullException(nameof(durationSelector));
            if (comparer == null)
                throw new ArgumentNullException(nameof(comparer));

            return CreateAsyncObservable<IGroupedAsyncObservable<TKey, TSource>>.From(
                source,
                (keySelector, durationSelector, comparer),
                static (source, state, observer) => GroupByUntilCore<TSource, TKey, TSource, TDuration>(source, observer, (o, d) => AsyncObserver.GroupByUntil(o, d, state.keySelector, state.durationSelector, state.comparer)));
        }

        public static IAsyncObservable<IGroupedAsyncObservable<TKey, TSource>> GroupByUntil<TSource, TKey, TDuration>(this IAsyncObservable<TSource> source, Func<TSource, ValueTask<TKey>> keySelector, Func<IGroupedAsyncObservable<TKey, TSource>, IAsyncObservable<TDuration>> durationSelector, int capacity)
        {
            if (source == null)
                throw new ArgumentNullException(nameof(source));
            if (keySelector == null)
                throw new ArgumentNullException(nameof(keySelector));
            if (durationSelector == null)
                throw new ArgumentNullException(nameof(durationSelector));
            if (capacity < 0)
                throw new ArgumentOutOfRangeException(nameof(capacity));

View on GitHub (pinned to 94b5d5ab91)