dotnet/reactive · error · ArgumentOutOfRangeException

Specified argument was out of the range of valid values…

Error message

Specified argument was out of the range of valid values. (Parameter 'period')

What it means

ControlScheduler.SchedulePeriodic requires a period of at least 1 millisecond because it is backed by a System.Windows.Forms.Timer, whose Interval property has the same lower bound (threshold taken from the WinForms Timer setter). Periods of TimeSpan.Zero, negative values, or sub-millisecond fractions throw ArgumentOutOfRangeException with Parameter 'period'.

Solutions

  1. Clamp the period to at least 1 ms: period = period < TimeSpan.FromMilliseconds(1) ? TimeSpan.FromMilliseconds(1) : period.
  2. If a tight/immediate loop is intended, use a different scheduler (Scheduler.Default / TaskPool) instead of ControlScheduler.
  3. Validate configured interval values at startup and fail fast with a clear message.

Example fix

// before
scheduler.SchedulePeriodic(state, TimeSpan.FromMilliseconds(config.IntervalMs), tick); // 0 → throws
// after
var period = TimeSpan.FromMilliseconds(Math.Max(config.IntervalMs, 1));
scheduler.SchedulePeriodic(state, period, tick);
Defensive patterns

Strategy: validation

Validate before calling

var min = TimeSpan.FromMilliseconds(1);
if (period < min) period = min;
scheduler.SchedulePeriodic(state, period, action);

Type guard

static bool IsValidPeriod(TimeSpan p) => p.TotalMilliseconds >= 1;

Try / catch

try { scheduler.SchedulePeriodic(state, period, action); } catch (ArgumentOutOfRangeException ex) when (ex.ParamName == "period") { /* clamp to 1ms and retry */ }

Prevention

When it happens

Trigger: scheduler.SchedulePeriodic(state, TimeSpan.Zero, action), negative TimeSpan, or TimeSpan.FromTicks(<10000) / TimeSpan.FromMilliseconds(0.5) — anything with TotalMilliseconds < 1.

Common situations: Computing the interval from a config value of 0 interpreted as 'as fast as possible'; dividing a TimeSpan and rounding down to zero; porting code from another scheduler (e.g. DefaultScheduler) that accepts zero or negative periods as immediate/tight loops.

Related errors


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

Appendix: source

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

        /// <summary>
        /// Schedules a periodic piece of work on the message loop associated with the control, using a Windows Forms Timer object.
        /// </summary>
        /// <typeparam name="TState">The type of the state passed to the scheduled action.</typeparam>
        /// <param name="state">Initial state passed to the action upon the first iteration.</param>
        /// <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 null.</exception>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="period"/> is less than one millisecond.</exception>
        public IDisposable SchedulePeriodic<TState>(TState state, TimeSpan period, Func<TState, TState> action)
        {
            //
            // Threshold derived from Interval property setter in ndp\fx\src\winforms\managed\system\winforms\Timer.cs.
            //
            if (period.TotalMilliseconds < 1)
            {
                throw new ArgumentOutOfRangeException(nameof(period));
            }

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

            var createTimer = new Func<IScheduler, TState, IDisposable>((scheduler1, state1) =>
            {
                var timer = new System.Windows.Forms.Timer();

                timer.Tick += (s, e) =>
                {
                    if (!_control.IsDisposed)
                    {
                        state1 = action(state1);
                    }
                };

View on GitHub (pinned to 94b5d5ab91)