stride3d/stride · error · Exception

AfterUpdate cannot be called out of the micro-thread…

Error message

AfterUpdate cannot be called out of the micro-thread context.

What it means

AfterUpdate() yields the current micro-thread until just after the next physics tick, mirroring NextUpdate(). It only works when the caller is on a Stride micro-thread with a SynchronizationContext, since TickAwaiter resumes through those; otherwise it throws immediately.

Solutions

  1. Invoke AfterUpdate() from an async Stride script running on a micro-thread.
  2. Use engine callbacks/events (e.g. after-tick hooks) for code outside the micro-thread context.
  3. Restructure the logic so the post-tick continuation lives inside the script's async flow.

Example fix

// before
await Task.Run(() => sim.AfterUpdate());
// after
public async Task MyScript() {
    var awaiter = sim.AfterUpdate(); // valid: on micro-thread
}
Defensive patterns

Strategy: validation

Validate before calling

if (Scheduler.CurrentMicroThread is null || SynchronizationContext.Current is null) {
    return; // cannot await AfterUpdate here
}
var awaiter = sim.AfterUpdate();

Try / catch

try { var a = sim.AfterUpdate(); } catch (Exception ex) { log.Warn("AfterUpdate requires micro-thread context", ex); }

Prevention

When it happens

Trigger: Calling BepuSimulation.AfterUpdate() from a raw Task, thread pool thread, unit test, or any code where Scheduler.CurrentMicroThread or SynchronizationContext.Current is null.

Common situations: Attempting post-physics-step synchronization from background workers or non-Stride test harnesses instead of from within Stride's script/micro-thread scheduling.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


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

Appendix: source

Thrown at sources/engine/Stride.BepuPhysics/Stride.BepuPhysics/BepuSimulation.cs:378

    /// <summary>
    /// Yields execution until right before the next physics tick
    /// </summary>
    /// <returns>Task that will resume next tick.</returns>
    public TickAwaiter NextUpdate()
    {
        if (Scheduler.CurrentMicroThread is null || SynchronizationContext.Current is null)
            throw new Exception($"{nameof(NextUpdate)} cannot be called out of the micro-thread context.");
        return new TickAwaiter(_preTickRunner, Scheduler.CurrentMicroThread, SynchronizationContext.Current);
    }

    /// <summary>
    /// Yields execution until right after the next physics tick
    /// </summary>
    /// <returns>Task that will resume next tick.</returns>
    public TickAwaiter AfterUpdate()
    {
        if (Scheduler.CurrentMicroThread is null || SynchronizationContext.Current is null)
            throw new Exception($"{nameof(AfterUpdate)} cannot be called out of the micro-thread context.");
        return new TickAwaiter(_postTickRunner, Scheduler.CurrentMicroThread, SynchronizationContext.Current);
    }

    /// <summary>
    /// Whether a physics test with <paramref name="mask"/> against <paramref name="collidable"/> should be performed or entirely ignored
    /// </summary>
    /// <returns>True when it should be performed, false when it should be ignored</returns>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public bool ShouldPerformPhysicsTest(CollisionMask mask, CollidableReference collidable)
    {
        var component = GetComponent(collidable);
        return mask.IsSet(component.CollisionLayer);
    }

    /// <summary>
    /// Finds the closest intersection between this ray and shapes in the simulation.
    /// </summary>
    /// <param name="origin">The start position for this ray</param>

View on GitHub (pinned to 96fad776d2)