stride3d/stride · error · InvalidOperationException

MicroThread is already completed but still posting…

Error message

MicroThread is already completed but still posting continuations.

What it means

MicroThreadSynchronizationContext.Post schedules a continuation delegate on the micro-thread. If the target micro-thread is already Completed, there is nothing left to resume, so Post throws InvalidOperationException rather than silently dropping the continuation.

Solutions

  1. Check microThread.State == MicroThreadState.Completed before calling Post
  2. Do not capture/use the micro-thread's SynchronizationContext after completion; marshal back through the scheduler with a fresh micro-thread if needed
  3. Fix ownership so continuations are posted before the thread completes (complete sources within the body)

Example fix

// before
syncContext.Post(d, state); // d may run after thread completed
// after
if (microThread.State != MicroThreadState.Completed)
    syncContext.Post(d, state);
Defensive patterns

Strategy: type-guard

Validate before calling

if (microThread.State == MicroThreadState.Completed) return;

Type guard

bool canPost = microThread.State != MicroThreadState.Completed;

Try / catch

try { context.Post(d, state); }
catch (InvalidOperationException) { /* continuation target completed; drop or reschedule */ }

Prevention

When it happens

Trigger: Posting continuations (e.g. continuations captured by await/Task sources) to a micro-thread after its body finished — typically a TaskCompletionSource or SynchronizationContext captured earlier and completed after the thread finished.

Common situations: A TaskCompletionSource held by the micro-thread is SetResult'ed later by external code; events firing after the script completed; continuation stored and invoked by a timer or other micro-thread after completion.

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 stride3d/stride@96fad776d2 (2026-09-14). Data as JSON: /api/errors/10175d3f49f327b4. Report an issue: GitHub.

Appendix: source

Thrown at sources/core/Stride.Core.MicroThreading/MicroThreadSynchronizationContext.cs:35

        return this;
    }

    public override void Post(SendOrPostCallback d, object? state)
    {
        // There is two case:
        // 1/ We are either in normal MicroThread inside Scheduler.Step() (CurrentThread test),
        // in which case we will directly execute the callback to avoid further processing from scheduler.
        // Also, note that Wait() sends us event that are supposed to come back into scheduler.
        // Note: As it will end up on the callstack, it might be better to Schedule it instead (to avoid overflow)?
        // 2/ Otherwise, we just received an external task continuation (i.e. Task.Sleep()), or a microthread triggering another,
        // so schedule it so that it comes back in our regular scheduler.
        if (microThread.Scheduler.RunningMicroThread == microThread)
        {
            d(state);
        }
        else if (microThread.State == MicroThreadState.Completed)
        {
            throw new InvalidOperationException("MicroThread is already completed but still posting continuations.");
        }
        else
        {
            microThread.ScheduleContinuation(microThread.ScheduleMode, d, state);
        }
    }

    MicroThread IMicroThreadSynchronizationContext.MicroThread => microThread;
}

View on GitHub (pinned to 96fad776d2)