dotnet/reactive · error · InvalidOperationException

The scheduler operation has already been awaited.

Error message

The scheduler operation has already been awaited.

What it means

A SchedulerOperation can only be awaited once: OnCompleted stores the continuation and throws InvalidOperationException ('The scheduler operation has already been awaited') on a second registration. This protects the single-continuation awaitable contract.

Solutions

  1. Await each SchedulerOperation exactly once; create a new operation for each await
  2. If retrying, re-issue the scheduled operation instead of re-awaiting the old one
  3. Capture the result via Task-like semantics only once and share the resulting Task instead

Example fix

// before
var op = scheduler.ScheduleAsync(...);
await op;
await op; // second await throws
// after
var op = scheduler.ScheduleAsync(...);
await op;
var op2 = scheduler.ScheduleAsync(...); // new operation for second await
await op2;
Defensive patterns

Strategy: try-catch

Try / catch

try { await operation; }
catch (InvalidOperationException) { /* already awaited: treat as no-op */ }

Prevention

When it happens

Trigger: Awaiting the same SchedulerOperation instance twice; calling OnCompleted again after a previous await completed or was cancelled; storing the operation and re-awaiting on retry logic.

Common situations: Retry loops that reuse the awaitable instead of obtaining a new operation; awaiting in both the original async method and a timeout/abandonment helper; caching the task-like object.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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

Appendix: source

Thrown at Rx.NET/Source/src/System.Reactive/Concurrency/SchedulerOperation.cs:98

        /// <summary>
        /// Completes the scheduler operation, throwing an OperationCanceledException in case cancellation was requested.
        /// </summary>
        public void GetResult() => _cancellationToken.ThrowIfCancellationRequested();

        /// <summary>
        /// Registers the continuation with the scheduler operation.
        /// </summary>
        /// <param name="continuation">Continuation to be run on the originating scheduler.</param>
        public void OnCompleted(Action continuation)
        {
            if (continuation == null)
            {
                throw new ArgumentNullException(nameof(continuation));
            }

            if (_continuation != null)
            {
                throw new InvalidOperationException(Strings_Core.SCHEDULER_OPERATION_ALREADY_AWAITED);
            }

            if (_postBackToOriginalContext)
            {
                var ctx = SynchronizationContext.Current;
                if (ctx != null)
                {
                    var original = continuation;
                    continuation = () =>
                    {
                        //
                        // No need for OperationStarted and OperationCompleted calls here;
                        // this code is invoked through await support and will have a way
                        // to observe its start/complete behavior, either through returned
                        // Task objects or the async method builder's interaction with the
                        // SynchronizationContext object.
                        //
                        // In general though, Rx doesn't play nicely with synchronization

View on GitHub (pinned to 94b5d5ab91)