stride3d/stride · error · InvalidOperationException

MicroThread was already started before.

Error message

MicroThread was already started before.

What it means

MicroThread.Start transitions the thread state from None to Starting atomically. If the instance was already started (state != None), it throws InvalidOperationException, since a micro-thread's lifetime is single-use; create a new instance to run again.

Solutions

  1. Create a new MicroThread instance for each run instead of restarting
  2. Track whether the thread was started (check State before Start)
  3. Use Scheduler context (e.g. microThread.Tasks / Spawn helpers) which allocate fresh instances

Example fix

// before
if (!myThread.State.HasFlag(MicroThreadState.Running)) myThread.Start(work); // reuses old instance
// after
var myThread = Scheduler.CreateMicroThread();
myThread.Start(work);
Defensive patterns

Strategy: validation

Validate before calling

if (microThread.State != MicroThreadState.None) microThread = scheduler.CreateMicroThread();

Try / catch

try { microThread.Start(work); }
catch (InvalidOperationException) { microThread = scheduler.CreateMicroThread(); microThread.Start(work); }

Prevention

When it happens

Trigger: Calling Start() twice on the same MicroThread instance, or calling Start after Schedule was invoked.

Common situations: Restarting a failed/completed micro-thread by calling Start again in a game update loop; reusing cached MicroThread objects across levels or asset reloads; callers like ScheduleBuildStep/Add that invoke Start on shared instances.

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/f9715c329ac18c0c. Report an issue: GitHub.

Appendix: source

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

        throw new NotImplementedException();
    }

    public void Remove()
    {
        throw new NotImplementedException();
    }

    /// <summary>
    /// Starts this <see cref="MicroThread"/> with the specified function.
    /// </summary>
    /// <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);
            }

View on GitHub (pinned to 96fad776d2)