dotnet/reactive · error · ArgumentNullException

Value cannot be null. (Parameter 'onNextAsync')

Error message

Value cannot be null. (Parameter 'onNextAsync')

What it means

The 4-argument ToLookup overload requires a non-null IEqualityComparer<TKey> because it constructs Lookup<TKey,TValue>(comparer) as the aggregate seed. A null comparer would make bucket lookups undefined, so ArgumentNullException(nameof(comparer)) is thrown before the pipeline is returned (ToLookup.cs:140).

Solutions

  1. Pass a valid IEqualityComparer<TKey> implementation
  2. Omit the comparer argument to use EqualityComparer<TKey>.Default via the 3-argument overload
  3. For case-insensitive keys use StringComparer.OrdinalIgnoreCase or similar

Example fix

// before
IEqualityComparer<int> comparer = GetComparer(); // returns null
var op = AsyncObservable.ToLookup(sink, x => x.Key, x => x.Value, comparer);
// after
var op = AsyncObservable.ToLookup(sink, x => x.Key, x => x.Value, EqualityComparer<int>.Default);
Defensive patterns

Strategy: validation

Validate before calling

var cmp = comparer ?? EqualityComparer<TKey>.Default;
var op = AsyncObservable.ToLookup(observer, keySelector, valueSelector, cmp);

Type guard

static IEqualityComparer<T> OrDefault<T>(IEqualityComparer<T>? c) => c ?? EqualityComparer<T>.Default;

Try / catch

try
{
    var op = AsyncObservable.ToLookup(observer, keySelector, valueSelector, comparer);
}
catch (ArgumentNullException ex) when (ex.ParamName == "comparer")
{
    var op = AsyncObservable.ToLookup(observer, keySelector, valueSelector); // default comparer
}

Prevention

When it happens

Trigger: Calling AsyncObservable.ToLookup(observer, keySelector, valueSelector, null); passing a comparer variable resolved from DI/config that is null.

Common situations: Comparer chosen based on settings (case-insensitive vs default) with an unhandled else-branch; caching a comparer in a dictionary lookup that missed; custom equality comparer class not registered.

Related errors


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

Appendix: source

Thrown at AsyncRx.NET/System.Reactive.Async/AsyncObservableExtensions.cs:17

// 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.Reactive;
using System.Threading.Tasks;

namespace System
{
    public static class AsyncObservableExtensions
    {
        public static ValueTask<IAsyncDisposable> SubscribeAsync<T>(this IAsyncObservable<T> source, Func<T, ValueTask> onNextAsync)
        {
            if (source == null)
                throw new ArgumentNullException(nameof(source));
            if (onNextAsync == null)
                throw new ArgumentNullException(nameof(onNextAsync));

            return source.SubscribeAsync(new AsyncObserver<T>(onNextAsync, ex => new ValueTask(Task.FromException(ex)), () => default));
        }

        public static ValueTask<IAsyncDisposable> SubscribeAsync<T>(this IAsyncObservable<T> source, Func<T, ValueTask> onNextAsync, Func<Exception, ValueTask> onErrorAsync)
        {
            if (source == null)
                throw new ArgumentNullException(nameof(source));
            if (onNextAsync == null)
                throw new ArgumentNullException(nameof(onNextAsync));
            if (onErrorAsync == null)
                throw new ArgumentNullException(nameof(onErrorAsync));

            return source.SubscribeAsync(new AsyncObserver<T>(onNextAsync, onErrorAsync, () => default));
        }

        public static ValueTask<IAsyncDisposable> SubscribeAsync<T>(this IAsyncObservable<T> source, Func<T, ValueTask> onNextAsync, Func<ValueTask> onCompletedAsync)
        {

View on GitHub (pinned to 94b5d5ab91)