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

ConcurrencyAbstractionLayerImpl.StartPeriodicTimer throws ArgumentOutOfRangeException when the period is negative. Rx permits TimeSpan.Zero (meaning 'as fast as possible, sequentially') but a negative period is meaningless, so the abstraction layer rejects it before creating the underlying timer.

Solutions

  1. Clamp or validate the period: if (period < TimeSpan.Zero) period = TimeSpan.Zero; before scheduling.
  2. Use TimeSpan.Zero intentionally if you want sequential as-fast-as-possible callbacks; otherwise pass a positive interval.
  3. Fix the upstream computation producing the negative TimeSpan (e.g. guard DateTime subtraction with Math.Max(TimeSpan.Zero, diff)).

Example fix

// before
var period = nextRun - DateTime.Now; // can be negative
cal.StartPeriodicTimer(tick, period);

// after
var period = nextRun - DateTime.Now;
if (period < TimeSpan.Zero) period = TimeSpan.Zero;
cal.StartPeriodicTimer(tick, period);
Defensive patterns

Strategy: validation

Validate before calling

if (period < TimeSpan.Zero) period = TimeSpan.Zero;
cal.StartPeriodicTimer(action, period);

Try / catch

try { cal.StartPeriodicTimer(action, period); } catch (ArgumentOutOfRangeException ex) when (ex.ParamName == "period") { /* clamp and retry with TimeSpan.Zero or positive period */ }

Prevention

When it happens

Trigger: Calling StartPeriodicTimer with a negative TimeSpan, typically from a computed value like TimeSpan.FromMilliseconds(-1) or a mis-subtracted duration, e.g. via SchedulePeriodic on DefaultScheduler.

Common situations: Computing intervals from subtracted DateTimes that produced a negative result, config files with negative polling intervals, or confusing dueTime semantics (negative dueTime is normalized to zero elsewhere, but period is not).

Related errors


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

Appendix: source

Thrown at Rx.NET/Source/src/System.Reactive/Concurrency/ConcurrencyAbstractionLayerImpl.cs:35

        private sealed class WorkItem
        {
            public WorkItem(Action<object?> action, object? state)
            {
                Action = action;
                State = state;
            }

            public Action<object?> Action { get; }
            public object? State { get; }
        }

        public IDisposable StartTimer(Action<object?> action, object? state, TimeSpan dueTime) => new Timer(action, state, Normalize(dueTime));

        public IDisposable StartPeriodicTimer(Action action, TimeSpan period)
        {
            if (period < TimeSpan.Zero)
            {
                throw new ArgumentOutOfRangeException(nameof(period));
            }

            //
            // The contract for periodic scheduling in Rx is that specifying TimeSpan.Zero as the period causes the scheduler to 
            // call back periodically as fast as possible, sequentially.
            //
            if (period == TimeSpan.Zero)
            {
                return new FastPeriodicTimer(action);
            }

            return new PeriodicTimer(action, period);
        }

        public IDisposable QueueUserWorkItem(Action<object?> action, object? state)
        {
            ThreadPool.QueueUserWorkItem(static itemObject =>
            {

View on GitHub (pinned to 94b5d5ab91)