dotnet/reactive · error · ArgumentNullException

action (Value cannot be null)

Error message

action (Value cannot be null)

What it means

This is the action-null branch of the same Schedule(Action) extension: the scheduler was valid but the Action delegate passed for scheduling is null, so ArgumentNullException with paramName "action" is thrown. Rx validates both arguments up front.

Solutions

  1. Pass a non-null lambda or delegate, even a no-op such as () => { }
  2. Check the delegate-producing code path and give it a default value before scheduling
  3. Guard the call: only schedule when the callback is non-null, otherwise run it inline or skip

Example fix

// before
Action callback = GetCallback(); // may be null
scheduler.Schedule(callback); // throws

// after
Action callback = GetCallback() ?? (() => { });
scheduler.Schedule(callback);
Defensive patterns

Strategy: validation

Validate before calling

if (action is null)
    action = () => { }; // no-op default
scheduler.Schedule(action);

Type guard

bool IsRunnable(Action? a) => a is not null;

Try / catch

try
{
    scheduler.Schedule(callback);
}
catch (ArgumentNullException ex) when (ex.ParamName == "action")
{
    scheduler.Schedule(() => { });
}

Prevention

When it happens

Trigger: Calling scheduler.Schedule(null) or passing a null delegate variable: e.g. scheduler.Schedule(myAction) where myAction is an uninitialized Action field, or a method returning null Action that is forwarded directly into Schedule.

Common situations: Optional callbacks wired only under certain conditions that end up null; reflection or configuration-built delegates that failed to resolve; refactors that removed a lambda assignment but left the Schedule call.

Related errors


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

Appendix: source

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

    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>
        /// Schedules an action to be executed.
        /// </summary>
        /// <param name="scheduler">Scheduler to execute the action on.</param>
        /// <param name="state">A state object to be passed to <paramref name="action"/>.</param>
        /// <param name="action">Action to execute.</param>

View on GitHub (pinned to 94b5d5ab91)