The current thread is not associated with the Dispatcher. Use InvokeAsync() to switch
execution to the Dispatcher when triggering rendering or component state. A component
called StateHasChanged() from a thread Blazor does not control, here a
System.Threading.Timer callback. Wrap the state change and the
StateHasChanged() in await InvokeAsync(...).
Take this one seriously in Blazor Server. The exception was thrown on a timer thread where nothing catches it, and the whole server process exited: every connected user lost the app, not only the tab with the timer.
The error
Unhandled exception. System.InvalidOperationException: The current thread is not associated with the Dispatcher. Use InvokeAsync() to switch execution to the Dispatcher when triggering rendering or component state.
at Microsoft.AspNetCore.Components.Dispatcher.AssertAccess()
at Microsoft.AspNetCore.Components.RenderTree.Renderer.AddToRenderQueue(Int32 componentId, RenderFragment renderFragment)
at Microsoft.AspNetCore.Components.ComponentBase.StateHasChanged()
at FixLab.Components.Pages.TimerPage.<OnInitialized>b__3_0(Object _) in ...\FixLab\Components\Pages\TimerPage.razor:line 18
at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location ---
at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
at System.Threading.TimerQueueTimer.Fire(Boolean isThreadPool)
Note the first words: Unhandled exception. is what .NET prints on its way out,
on the process's error stream, not a logged request failure. The component behind it was a
waiting-room countdown:
timer = new Timer(_ =>
{
secondsLeft--;
StateHasChanged();
}, null, 2000, 1000);
Why it happens
Each Blazor Server circuit has a Dispatcher that runs that user's component code one piece at
a time, so renders never overlap. Lifecycle methods and UI event handlers already run on it,
which is why StateHasChanged() normally just works. A
System.Threading.Timer calls back on a thread-pool thread instead.
StateHasChanged() asks the renderer to queue a render, the renderer first checks
that it is on its Dispatcher (the AssertAccess line in the trace), and it throws.
A Task.Run body or an event raised by a background service lands on the same
kind of thread.
Where it throws decides the damage. A timer callback has no caller to catch the exception, so .NET treats it as unhandled and ends the process. In the test the browser still said "Next call in 30 s" while the server behind it was gone, and after a restart the tab that was still open on the page brought the new process down within seconds.
The fix
timer = new Timer(async _ =>
{
await InvokeAsync(() =>
{
secondsLeft--;
StateHasChanged();
});
}, null, 2000, 1000);
InvokeAsync queues the lambda onto the component's Dispatcher, so the change and
the render happen in turn with everything else the circuit does. Moving
secondsLeft-- inside matters as well; the message says "rendering or component
state", not only rendering. When there is no state change to move, the short form is
await InvokeAsync(StateHasChanged);. Keep the component's Dispose
that disposes the timer, so it stops when the user leaves the page. With the fix the countdown
read 24 and then 21 three seconds later, the process stayed up, and its error stream stayed
empty.
How it was reproduced
A fresh dotnet new blazor -o FixLab --interactivity Server project on .NET SDK
10.0.401 (ASP.NET Core runtime 10.0.12, no extra packages). A page with
@rendermode InteractiveServer and @implements IDisposable started
the timer in OnInitialized, due after two seconds and then every second. The
page was opened in a browser; about two seconds later the app process exited with the trace
above on standard error. The fixed page ran the countdown with the process alive.
Frequently asked
- How do I call StateHasChanged from a timer in Blazor?
- Wrap it in InvokeAsync: await InvokeAsync(StateHasChanged), or put the state change and StateHasChanged in the same InvokeAsync lambda. InvokeAsync runs the code on the component's Dispatcher, the only place it is allowed to render.
- Why did my Blazor Server app crash with 'The current thread is not associated with the Dispatcher'?
- The exception was thrown inside a System.Threading.Timer callback, where nothing catches it, so .NET ended the whole process. In Blazor Server every connected user shares that process, so everyone loses the app, not only the page with the timer.
- Does InvokeAsync apply to Task.Run and service events too?
- Yes. Code that did not start in a Blazor lifecycle method or UI event handler, such as a Task.Run body, a timer callback or an event raised by a singleton service, must go through InvokeAsync before it changes component state or calls StateHasChanged.
More decoded errors in the Fixes category. The live
waiting-room board in the SignalR part of From
Prompt to Production uses the same await InvokeAsync(StateHasChanged) inside
its SignalR event handlers.