dotnet/wpf · error · InvalidOperationException

The thread may not wait on operations that are already…

Error message

The thread may not wait on operations that are already executing on the same thread.

What it means

DispatcherOperation.Wait throws InvalidOperationException when the thread that is currently dispatching the operation tries to wait on that same operation while it is executing. Blocking would deadlock the thread, so WPF detects the condition and throws instead of hanging.

Solutions

  1. Do not wait on an operation from within its own executing delegate - restructure so the continuation happens after execution
  2. Use async/await on the DispatcherOperation (await operation.GetAwaiter().GetResult() replaced by await operation.Task) instead of blocking
  3. If work must run after the operation completes, chain a continuation (ContinueWith / InvokeAsync) instead of blocking
  4. Split the work so the waiting code runs on a different thread or after PushFrame returns

Example fix

// before (inside the operation's delegate)
DispatcherOperation op = Dispatcher.CurrentDispatcher.InvokeAsync(() => Work());
op.Wait(); // throws: waiting on own operation
// after
await Dispatcher.CurrentDispatcher.InvokeAsync(() => Work());
Defensive patterns

Strategy: validation

Validate before calling

bool isDeadlockRisk(DispatcherOperation op) =>
    op.Status == DispatcherOperationStatus.Executing &&
    op.Dispatcher.Thread == Thread.CurrentThread;

Type guard

bool canWaitSafely(DispatcherOperation op) =>
    !(op.Status == DispatcherOperationStatus.Executing && op.Dispatcher.Thread == Thread.CurrentThread);

Try / catch

try
{
    operation.Wait(timeout);
}
catch (InvalidOperationException ex) when (ex.Message.Contains("may not wait"))
{
    // restructure: do not wait on your own operation
}

Prevention

When it happens

Trigger: Calling Wait() (or EndInvoke/Result) on a DispatcherOperation from inside the delegate that the operation is currently executing on the same Dispatcher thread.

Common situations: A queued work item waits on its own operation's result, or nested code inside a Dispatcher.Invoke callback calls Wait on the enclosing operation; typical in legacy code migrated to TPL where blocking waits remain.

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/wpf@81131a70a4 (2026-09-14). Data as JSON: /api/errors/63dca2bb4edfad37. Report an issue: GitHub.

Appendix: source

Thrown at src/Microsoft.DotNet.Wpf/src/WindowsBase/System/Windows/Threading/DispatcherOperation.cs:194

        /// <returns>
        ///     The status of the operation.  To obtain the return value
        ///     of the invoked delegate, use the the Result property.
        /// </returns>
        public DispatcherOperationStatus Wait(TimeSpan timeout)
        {
            if((_status == DispatcherOperationStatus.Pending || _status == DispatcherOperationStatus.Executing) &&
                timeout.TotalMilliseconds != 0)
            {
                if(_dispatcher.Thread == Thread.CurrentThread)
                {
                    if(_status == DispatcherOperationStatus.Executing)
                    {
                        // We are the dispatching thread, and the current operation state is
                        // executing, which means that the operation is in the middle of
                        // executing (on this thread) and is trying to wait for the execution
                        // to complete.  Unfortunately, the thread will now deadlock, so
                        // we throw an exception instead.
                        throw new InvalidOperationException(SR.ThreadMayNotWaitOnOperationsAlreadyExecutingOnTheSameThread);
                    }
                    
                    // We are the dispatching thread for this operation, so
                    // we can't block.  We will push a frame instead.
                    DispatcherOperationFrame frame = new DispatcherOperationFrame(this, timeout);
                    Dispatcher.PushFrame(frame);
                }
                else
                {
                    // We are some external thread, so we can just block.  Of
                    // course this means that the Dispatcher (queue)for this
                    // thread (if any) is now blocked.  The COM STA model 
                    // suggests that we should pump certain messages so that
                    // back-communication can happen.  Underneath us, the CLR
                    // will pump the STA apartment for us, and we will allow 
                    // the UI thread for a context to call
                    // Invoke(Priority.Max, ...) without going through the
                    // blocked queue.

View on GitHub (pinned to 81131a70a4)