dotnet/reactive · error · ArgumentNullException

Value cannot be null. (Parameter 'onErrorAsync')

Error message

Value cannot be null. (Parameter 'onErrorAsync')

What it means

UnsafeAsyncObserver's constructor validates all three delegate arguments and throws ArgumentNullException when onErrorAsync is null. The library requires an explicit error-handling delegate so every observer has a well-defined OnErrorAsync path; there is no default no-op handler for this unsafe/fast-path type. This is an eager, fail-fast guard thrown at construction time, not during event delivery.

Solutions

  1. Pass a non-null error handler delegate, e.g. ex => ValueTask.CompletedTask or one that logs the exception
  2. Check every UnsafeAsyncObserver construction site and supply all three delegates (onNextAsync, onErrorAsync, onCompletedAsync)
  3. Wrap construction in a helper factory with default delegates so callers can omit error handling explicitly

Example fix

// before
var observer = new UnsafeAsyncObserver<int>(
    x => ValueTask.CompletedTask,
    null,
    () => ValueTask.CompletedTask);
// after
var observer = new UnsafeAsyncObserver<int>(
    x => ValueTask.CompletedTask,
    ex => Console.Error.WriteLine(ex),
    () => ValueTask.CompletedTask);
Defensive patterns

Strategy: validation

Validate before calling

if (onErrorAsync == null) throw new ArgumentException("onErrorAsync delegate is required");

Type guard

static bool IsValidObserver<T>(Func<T, ValueTask> n, Func<Exception, ValueTask> e, Func<ValueTask> c) => n != null && e != null && c != null;

Try / catch

try { var obs = new UnsafeAsyncObserver<int>(onNext, onError, onCompleted); } catch (ArgumentNullException ex) { /* ex.ParamName == "onErrorAsync" */ }

Prevention

When it happens

Trigger: Calling new UnsafeAsyncObserver<T>(onNextAsync, null, onCompletedAsync) — i.e. passing a null Func<Exception, ValueTask> as the second constructor argument.

Common situations: Writing a custom subscription that handles OnNext and OnCompleted but forgets the error callback; refactoring code that previously swallowed exceptions; mapping from an observer abstraction with optional error handling onto this API that requires all three delegates.

Related errors


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

Appendix: source

Thrown at AsyncRx.NET/System.Reactive.Async/Internal/UnsafeAsyncObserver.cs:18

// 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.Tasks;

namespace System.Reactive
{
    public class UnsafeAsyncObserver<T> : IAsyncObserver<T>
    {
        private readonly Func<T, ValueTask> _onNextAsync;
        private readonly Func<Exception, ValueTask> _onErrorAsync;
        private readonly Func<ValueTask> _onCompletedAsync;

        public UnsafeAsyncObserver(Func<T, ValueTask> onNextAsync, Func<Exception, ValueTask> onErrorAsync, Func<ValueTask> onCompletedAsync)
        {
            _onNextAsync = onNextAsync ?? throw new ArgumentNullException(nameof(onNextAsync));
            _onErrorAsync = onErrorAsync ?? throw new ArgumentNullException(nameof(onErrorAsync));
            _onCompletedAsync = onCompletedAsync ?? throw new ArgumentNullException(nameof(onCompletedAsync));
        }

        public ValueTask OnCompletedAsync() => _onCompletedAsync();

        public ValueTask OnErrorAsync(Exception error) => _onErrorAsync(error ?? throw new ArgumentNullException(nameof(error)));

        public ValueTask OnNextAsync(T value) => _onNextAsync(value);
    }
}

View on GitHub (pinned to 94b5d5ab91)