dotnet/reactive · error · ArgumentNullException

scheduler (Value cannot be null)

Error message

scheduler (Value cannot be null)

What it means

The IScheduler.Schedule(Action) extension requires a non-null scheduler and a non-null action. When the scheduler argument is null it throws ArgumentNullException with paramName "scheduler" before doing any work. Rx throws eagerly so scheduling failures surface at the call site rather than inside the scheduler.

Solutions

  1. Pass a valid scheduler instance such as Scheduler.Default, Scheduler.CurrentThread, Scheduler.ThreadPool, or NewThreadScheduler.Default
  2. Fix the DI registration or factory so the IScheduler dependency resolves non-null
  3. Verify the variable holding the scheduler is assigned before the Schedule call
  4. Both scheduler and action are validated — make sure neither is null

Example fix

// before
IScheduler scheduler;
scheduler.Schedule(() => DoWork()); // NRE-prone: scheduler is null

// after
IScheduler scheduler = Scheduler.Default;
scheduler.Schedule(() => DoWork());
Defensive patterns

Strategy: validation

Validate before calling

if (scheduler is null || action is null)
    throw new InvalidOperationException("Both scheduler and action are required for Schedule.");
scheduler.Schedule(action);

Type guard

bool CanSchedule(IScheduler? s, Action? a) => s is not null && a is not null;

Try / catch

try
{
    scheduler.Schedule(work);
}
catch (ArgumentNullException ex) when (ex.ParamName == "scheduler")
{
    Scheduler.Default.Schedule(work);
}

Prevention

When it happens

Trigger: Calling scheduler.Schedule(someAction) where the scheduler receiver is null — e.g. Scheduler.Default replaced with null, a null scheduler field/property, or a null returned by a scheduler factory.

Common situations: Uninitialized IScheduler dependencies in services; DI misconfiguration (no IScheduler binding); test setups where a scheduler mock was not wired; code that stored Scheduler.Default in a static that was cleared.

Related errors


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

Appendix: source

Thrown at Rx.NET/Source/src/System.Reactive/Concurrency/Scheduler.Simple.cs:22

using System.Reactive.Disposables;

namespace System.Reactive.Concurrency
{
    public static partial class Scheduler
    {
        /// <summary>
        /// Schedules an action to be executed.
        /// </summary>
        /// <param name="scheduler">Scheduler to execute the action on.</param>
        /// <param name="action">Action to execute.</param>
        /// <returns>The disposable object used to cancel the scheduled action (best effort).</returns>
        /// <exception cref="ArgumentNullException"><paramref name="scheduler"/> or <paramref name="action"/> is <c>null</c>.</exception>
        public static IDisposable Schedule(this IScheduler scheduler, Action action)
        {
            if (scheduler == null)
            {
                throw new ArgumentNullException(nameof(scheduler));
            }

            if (action == null)
            {
                throw new ArgumentNullException(nameof(action));
            }

            // Surprisingly, passing the method group of Invoke will create a fresh
            // delegate each time although it's static, while an anonymous
            // lambda without the need of a closure will be cached.
            // Once Roslyn supports caching delegates for method groups,
            // the anonymous lambda can be replaced by the method group again. Until then,
            // to avoid the repetition of code, the call to Invoke is left intact.
            // Watch https://github.com/dotnet/roslyn/issues/5835
            return scheduler.Schedule(action, static (_, a) => Invoke(a));
        }

        /// <summary>

View on GitHub (pinned to 94b5d5ab91)