dotnet/reactive · error · ArgumentNullException

Value cannot be null. (Parameter 'action')

Error message

Value cannot be null. (Parameter 'action')

What it means

ControlScheduler.Schedule<TState>(state, action) invokes the action on the scheduler's control thread and therefore requires a non-null action delegate. The ArgumentNullException guard runs before the IsDisposed check, so a null action always throws even if the control is already disposed.

Solutions

  1. Pass a non-null delegate: scheduler.Schedule(state, (sched, st) => { ...; return Disposable.Empty; }).
  2. Guard the delegate before scheduling and fall back to a no-op returning Disposable.Empty.
  3. Verify argument order in the Schedule call so a state value is not passed where the Func is expected.

Example fix

// before
scheduler.Schedule(state, _work); // _work null
// after
var work = _work ?? ((sched, st) => Disposable.Empty);
scheduler.Schedule(state, work);
Defensive patterns

Strategy: validation

Validate before calling

if (action == null) action = (sched, st) => Disposable.Empty;
scheduler.Schedule(state, action);

Type guard

static bool HasWork<TState>(Func<IScheduler, TState, IDisposable> a) => a is not null;

Try / catch

try { scheduler.Schedule(state, action); } catch (ArgumentNullException ex) when (ex.ParamName == "action") { /* substitute no-op or log */ }

Prevention

When it happens

Trigger: Calling scheduler.Schedule(state, null) directly, or Rx operators receiving a null work delegate, e.g. a lambda variable that was never assigned or a Func field left null after refactoring.

Common situations: Building schedules dynamically from a dictionary of delegates where the lookup missed; passing a method group whose containing object method is conditionally compiled out; typo passing wrong argument order so null lands in the action parameter.

Related errors


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

Appendix: source

Thrown at Rx.NET/Source/src/System.Reactive/Platforms/Desktop/Concurrency/ControlScheduler.cs:48

        /// <summary>
        /// Gets the control associated with the ControlScheduler.
        /// </summary>
        public Control Control => _control;

        /// <summary>
        /// Schedules an action to be executed on the message loop associated with the control.
        /// </summary>
        /// <typeparam name="TState">The type of the state passed to the scheduled action.</typeparam>
        /// <param name="state">State passed to the action to be executed.</param>
        /// <param name="action">Action to be executed.</param>
        /// <returns>The disposable object used to cancel the scheduled action (best effort).</returns>
        /// <exception cref="ArgumentNullException"><paramref name="action"/> is null.</exception>
        public override IDisposable Schedule<TState>(TState state, Func<IScheduler, TState, IDisposable> action)
        {
            if (action == null)
            {
                throw new ArgumentNullException(nameof(action));
            }

            if (_control.IsDisposed)
            {
                return Disposable.Empty;
            }

            var d = new SingleAssignmentDisposable();

            _control.BeginInvoke(new Action(() =>
            {
                if (!_control.IsDisposed && !d.IsDisposed)
                {
                    d.Disposable = action(this, state);
                }
            }));

            return d;

View on GitHub (pinned to 94b5d5ab91)