stride3d/stride · error · InvalidOperationException

MicroThread completed in an invalid state.

Error message

MicroThread completed in an invalid state.

What it means

After the micro-thread's function completes, the wrapper asserts the state is still Running before marking it Completed. If the body (or its continuations) already changed State — e.g. set it to Canceled/Failed or completed via another path — Start throws InvalidOperationException 'MicroThread completed in an invalid state.'

Solutions

  1. Remove code inside the micro-thread body that mutates MicroThread.State or triggers completion directly
  2. Let cancellation propagate via OperationCanceledException instead of manually setting state
  3. Ensure the awaited function doesn't complete the micro-thread itself (e.g. double-awaiting a completion task)

Example fix

// before
State = MicroThreadState.Completed; // inside micro-thread body
// after
// do nothing; framework sets Completed when the awaited function returns
Defensive patterns

Strategy: try-catch

Try / catch

try { await microThreadFunction(); }
catch (InvalidOperationException) { /* state was mutated inside body; audit body for direct State writes */ }

Prevention

When it happens

Trigger: The awaited microThreadFunction returned after code inside it modified State directly, called Unschedule/completion APIs, or nested completion logic mutated the state machine.

Common situations: User script code calling low-level state setters or completion methods inside a micro-thread; duplicate completion logic firing when the function resolves; cancellation racing with completion.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of stride3d/stride@96fad776d2 (2026-09-14). Data as JSON: /api/errors/6d1c03503dbdd7b7. Report an issue: GitHub.

Appendix: source

Thrown at sources/core/Stride.Core.MicroThreading/MicroThread.cs:177

    /// <param name="microThreadFunction">The micro thread function.</param>
    /// <param name="scheduleMode">The schedule mode.</param>
    /// <exception cref="System.InvalidOperationException">MicroThread was already started before.</exception>
    public void Start(Func<Task> microThreadFunction, ScheduleMode scheduleMode = ScheduleMode.Last)
    {
        // TODO: Interlocked compare exchange?
        if (Interlocked.CompareExchange(ref state, (int)MicroThreadState.Starting, (int)MicroThreadState.None) != (int)MicroThreadState.None)
            throw new InvalidOperationException("MicroThread was already started before.");

        Func<Task> wrappedMicroThreadFunction = async () =>
        {
            try
            {
                State = MicroThreadState.Running;

                await microThreadFunction();

                if (State != MicroThreadState.Running)
                    throw new InvalidOperationException("MicroThread completed in an invalid state.");
                State = MicroThreadState.Completed;
            }
            catch (OperationCanceledException e)
            {
                // Exit gracefully on cancellation exceptions
                SetException(e);
            }
            catch (Exception e)
            {
                Scheduler.Log.Error("Unexpected exception while executing a micro-thread.", e);
                SetException(e);
            }
            finally
            {
                lock (Scheduler.AllMicroThreads)
                {
                    Scheduler.AllMicroThreads.Remove(AllLinkedListNode);
                }

View on GitHub (pinned to 96fad776d2)