dotnet/reactive · error · ArgumentNullException

ArgumentNullException: action

Error message

ArgumentNullException: action

What it means

After validating the period, SchedulePeriodic validates the action delegate and throws ArgumentNullException (named action) when it is null. The periodic timer created from the DispatcherQueue would have nothing to invoke, so null is rejected up front.

Solutions

  1. Supply a concrete periodic action; use a no-op body returning the state if nothing needs doing.
  2. Null-check the callback at the call site before scheduling.
  3. Fix upstream assignment so the periodic callback is initialized.

Example fix

// before
scheduler.SchedulePeriodic(state, TimeSpan.FromSeconds(30), null);
// after
scheduler.SchedulePeriodic(state, TimeSpan.FromSeconds(30), st => { Heartbeat(st); return st; });
Defensive patterns

Strategy: validation

Validate before calling

if (action == null) throw new ArgumentException("action required");
scheduler.SchedulePeriodic(state, period, action);

Type guard

bool Schedulable<TState>(Func<TState, TState>? a) => a is not null;

Try / catch

try { scheduler.SchedulePeriodic(state, period, action); }
catch (ArgumentNullException ex) when (ex.ParamName == "action") { /* fix caller */ }

Prevention

When it happens

Trigger: Calling scheduler.SchedulePeriodic(state, TimeSpan.FromMinutes(1), null) or a wrapper forwarding a null periodic callback.

Common situations: Periodic poll/heartbeat helpers with optional callbacks; delegates lost during refactoring; test scaffolding passing null placeholders.

Related errors


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

Appendix: source

Thrown at Rx.NET/Source/src/System.Reactive.WindowsRuntime/System.Reactive.Concurrency/CoreDispatcherScheduler.cs:249

        /// <param name="period">Period for running the work periodically.</param>
        /// <param name="action">Action to be executed, potentially updating the state.</param>
        /// <returns>The disposable object used to cancel the scheduled recurring action (best effort).</returns>
        /// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c>.</exception>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="period"/> is less than <see cref="TimeSpan.Zero"/>.</exception>
        public IDisposable SchedulePeriodic<TState>(TState state, TimeSpan period, Func<TState, TState> action)
        {
            //
            // According to MSDN documentation, the default is TimeSpan.Zero, so that's definitely valid.
            // Empirical observation - negative values seem to be normalized to TimeSpan.Zero, but let's not go there.
            //
            if (period < TimeSpan.Zero)
            {
                throw new ArgumentOutOfRangeException(nameof(period));
            }

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

            var timer = CreateDispatcherQueue().CreateTimer();

            var state1 = state;

            timer.Tick += (o, e) =>
            {
                state1 = action(state1);
            };

            timer.Interval = period;
            timer.Start();

            return Disposable.Create(() =>
            {
                var t = Interlocked.Exchange(ref timer, null);
                if (t != null)

View on GitHub (pinned to 94b5d5ab91)