dotnet/aspnetcore · error · InvalidOperationException

The current thread is not associated with the Dispatcher. Us

Error message

The current thread is not associated with the Dispatcher. Use InvokeAsync() to switch execution to the Dispatcher when triggering rendering or component state.

What it means

Thrown by Dispatcher.AssertAccess when the calling thread is not the one associated with the Blazor synchronization context (the Dispatcher). Blazor component state and rendering must only be touched from the dispatcher's thread; calling rendering APIs or mutating component state from a background thread or external callback triggers this InvalidOperationException. The fix is to marshal the work back via InvokeAsync.

Source

Thrown at src/Components/Components/src/Dispatcher.cs:32

    /// <summary>
    /// Creates a default instance of <see cref="Dispatcher"/>.
    /// </summary>
    /// <returns>A <see cref="Dispatcher"/> instance.</returns>
    public static Dispatcher CreateDefault() => new RendererSynchronizationContextDispatcher();

    /// <summary>
    /// Provides notifications of unhandled exceptions that occur within the dispatcher.
    /// </summary>
    internal event UnhandledExceptionEventHandler? UnhandledException;

    /// <summary>
    /// Validates that the currently executing code is running inside the dispatcher.
    /// </summary>
    public void AssertAccess()
    {
        if (!CheckAccess())
        {
            throw new InvalidOperationException(
                "The current thread is not associated with the Dispatcher. " +
                "Use InvokeAsync() to switch execution to the Dispatcher when " +
                "triggering rendering or component state.");
        }
    }

    /// <summary>
    /// Returns a value that determines whether using the dispatcher to invoke a work item is required
    /// from the current context.
    /// </summary>
    /// <returns><c>true</c> if invoking is required, otherwise <c>false</c>.</returns>
    public abstract bool CheckAccess();

    /// <summary>
    /// Invokes the given <see cref="Action"/> in the context of the associated <see cref="Renderer"/>.
    /// </summary>
    /// <param name="workItem">The action to execute.</param>
    /// <returns>A <see cref="Task"/> that will be completed when the action has finished executing.</returns>

View on GitHub (pinned to 294cab2f9b)

Solutions

  1. Wrap the state-mutating/rendering code in await InvokeAsync(() => { ... }).
  2. Use a dispatcher-aware timer or post the callback through InvokeAsync.
  3. For event handlers from external sources, capture the Dispatcher reference and always InvokeAsync through it.

Example fix

// before
private async Task LoadAsync()
{
    _data = await httpClient.GetFromJsonAsync<Data>("/api");
    StateHasChanged(); // may throw if off-dispatcher
}

// after
private async Task LoadAsync()
{
    _data = await httpClient.GetFromJsonAsync<Data>("/api");
    await InvokeAsync(StateHasChanged);
}
Defensive patterns

Strategy: validation

Validate before calling

if (!Dispatcher.CheckAccess())
    throw new InvalidOperationException("Not on dispatcher thread; use InvokeAsync.");
// or simply:
await Dispatcher.InvokeAsync(() => { /* work */ });

Prevention

When it happens

Trigger: Calling StateHasChanged(), component property setters, or CascadingValueSource methods from a Task continuation, a Timer.Elapsed handler, an event handler in a non-Blazor context, or any thread that isn't the dispatcher thread. AssertAccess is called at Dispatcher.cs:28-37.

Common situations: Using System.Timers.Timer instead of a dispatcher-aware timer; awaiting a long task and then touching state without InvokeAsync; subscribing to a non-Blazor event (e.g., a static event or message bus) and modifying component state in the handler.

Related errors


AI-assisted analysis of dotnet/aspnetcore@294cab2f9b (2026-08-06). Data as JSON: /api/errors/a1758347a51b5ba0. Report an issue: GitHub.