dotnet/reactive · error · ArgumentNullException

action (Parameter 'action')

Error message

action (Parameter 'action')

What it means

ImmediateScheduler.Schedule<TState>(state, action) throws ArgumentNullException when action is null. The immediate scheduler runs the action synchronously on the calling thread, and since there is no queue, it validates the delegate at the very start.

Solutions

  1. Pass a non-null delegate to Schedule; trace the null back to the lambda/field that was never assigned.
  2. Validate delegates at your API boundary before invoking Rx scheduling.
  3. Use a fallback no-op action if executing nothing is acceptable: (s, st) => Disposable.Empty.

Example fix

// before
Action? userCallback = maybeNull;
Scheduler.Immediate.Schedule(0, (s, st) => { userCallback!(); return Disposable.Empty; }); // throws if userCallback null

// after
if (userCallback != null)
    Scheduler.Immediate.Schedule(0, (s, st) => { userCallback(); return Disposable.Empty; });
Defensive patterns

Strategy: validation

Validate before calling

if (action is null) return Disposable.Empty; // or throw earlier with context
Scheduler.Immediate.Schedule(state, action);

Type guard

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

Try / catch

try { Scheduler.Immediate.Schedule(state, action); }
catch (ArgumentNullException ex) when (ex.ParamName == "action")
{
    // delegate was null upstream
}

Prevention

When it happens

Trigger: Calling Scheduler.Immediate.Schedule(state, null), or an Rx operator/timer helper (e.g. via InvokeRec, ScheduleAsync) receiving a null delegate and forwarding it to the immediate scheduler.

Common situations: Unit tests exercising Rx operators with hand-written schedulers; code paths where a lambda is built from a nullable callback and immediately executed synchronously via Immediate.

Related errors


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

Appendix: source

Thrown at Rx.NET/Source/src/System.Reactive/Concurrency/ImmediateScheduler.cs:38

        /// <summary>
        /// Gets the singleton instance of the immediate scheduler.
        /// </summary>
        public static ImmediateScheduler Instance => StaticInstance.Value;

        /// <summary>
        /// Schedules an action to be executed.
        /// </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));
            }

            return action(new AsyncLockScheduler(), state);
        }

        /// <summary>
        /// Schedules an action to be executed after dueTime.
        /// </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>
        /// <param name="dueTime">Relative time after which to execute the action.</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, TimeSpan dueTime, Func<IScheduler, TState, IDisposable> action)
        {
            if (action == null)
            {

View on GitHub (pinned to 94b5d5ab91)