dotnet/aspnetcore · error · Error
The circuit associated with this dispatcher is no longer…
Error message
The circuit associated with this dispatcher is no longer available.
What it means
`throwIfDispatchingWhenDisposed` guards every dispatcher method (`beginInvokeDotNetFromJS`, `endInvokeJSFromDotNet`, `sendByteArray`) so no JS->.NET calls are sent over a dead circuit. Once `dispose()`/`disposeCore()` sets `_disposed = true`, any subsequent dispatch attempt throws — the underlying SignalR connection is gone and the server-side circuit is being torn down.
Solutions
- Cancel/short-circuit JS-side work that may call back into .NET when the circuit is disposing (track a disposed flag in your interop wrapper).
- Wrap fire-and-forget callbacks in a guard that checks dispatcher availability before invoking.
- Prefer `IAsyncDisposable`/`@implements IAsyncDisposable` to tear down JS resources before the circuit is fully disposed.
- Surface this as a recoverable condition in your error UI rather than an unhandled promise rejection.
Example fix
// before
setTimeout(async () => {
await DotNet.invokeMethodAsync('MyAssembly', 'LateCallback'); // throws if disposed
}, 5000);
// after
let cancelled = false;
export function dispose() { cancelled = true; }
setTimeout(async () => {
if (cancelled) return;
try { await DotNet.invokeMethodAsync('MyAssembly', 'LateCallback'); }
catch { /* circuit gone - ignore */ }
}, 5000); Defensive patterns
Strategy: try-catch
Validate before calling
let circuitDisposed = false;
export function markDisposed() { circuitDisposed = true; }
async function safeInvoke(assembly: string, method: string, ...args: unknown[]) {
if (circuitDisposed) return;
return DotNet.invokeMethodAsync(assembly, method, ...args);
} Try / catch
try { await DotNet.invokeMethodAsync(asm, method); }
catch (e) { if (/no longer available/.test(e.message)) { /* circuit disposed - swallow */ return; } throw e; } Prevention
- Track a disposed flag in your JS interop wrapper and short-circuit callbacks.
- Cancel JS-side timers/intervals that may call into .NET after teardown.
- Implement IAsyncDisposable to tear down JS resources in order.
- Treat 'circuit no longer available' as recoverable, not a hard failure.
When it happens
Trigger: Any JS interop call routed through the dispatcher after `_disposed` becomes true: a pending `DotNet.invokeMethodAsync`, an `[JSInvokable]` callback completion, or a byte-array send that races with disposal.
Common situations: Long-running JS work calling back into .NET after the user navigated away or the circuit dropped; a `DotNet.invokeMethodAsync` fired from a setTimeout/interval that outlived the circuit; event handlers firing during teardown; streaming data sent after disconnect.
Related errors
- Cannot start a disposed circuit.
- Invalid sequence number or identifier
- Cannot start the circuit until Blazor Server has started.
- Circuit host not initialized.
- Circuit state is already in progress
AI-assisted analysis of dotnet/aspnetcore@3600ca084e (2026-08-11).
Data as JSON: /api/errors/83da029c2d66ed0f.
Report an issue: GitHub.
Appendix: source
Thrown at src/Components/Web.JS/src/Platform/Circuits/CircuitManager.ts:528
// Implements DotNet.DotNetCallDispatcher
public endInvokeJSFromDotNet(asyncHandle: number, succeeded: boolean, argsJson: any): void {
this.throwIfDispatchingWhenDisposed();
if (asyncHandle !== 0) {
this.changeActivity(-1);
}
this._connection!.send('EndInvokeJSFromDotNet', asyncHandle, succeeded, argsJson);
}
// Implements DotNet.DotNetCallDispatcher
public sendByteArray(id: number, data: Uint8Array): void {
this.throwIfDispatchingWhenDisposed();
this._connection!.send('ReceiveByteArray', id, data);
}
private throwIfDispatchingWhenDisposed() {
if (this._disposed) {
throw new Error('The circuit associated with this dispatcher is no longer available.');
}
}
public sendLocationChanged(uri: string, state: string | undefined, intercepted: boolean): Promise<void> {
return this._connection!.send('OnLocationChanged', uri, state, intercepted);
}
public sendLocationChanging(callId: number, uri: string, state: string | undefined, intercepted: boolean): Promise<void> {
return this._connection!.send('OnLocationChanging', callId, uri, state, intercepted);
}
public sendJsDataStream(data: ArrayBufferView | Blob, streamId: number, chunkSize: number) {
this.changeActivity(1);
return sendJSDataStream(this._connection!, data, streamId, chunkSize, () => this.changeActivity(-1));
}
public resolveElement(sequenceOrIdentifier: string): LogicalElement {
// It may be a root component added by JSView on GitHub (pinned to 3600ca084e)