dotnet/aspnetcore · error · Error

Cannot start a disposed circuit.

Error message

Cannot start a disposed circuit.

What it means

`CircuitManager.start()` bootstraps the SignalR circuit for a Blazor Server/Global app. Before doing any work it checks `isDisposedOrDisposing()`, which is true once `_disposePromise` is set (i.e. `dispose()` has been called). Starting a circuit that is already tearing down is a lifecycle violation, so it throws immediately rather than producing a half-initialized connection.

Solutions

  1. Guard any custom `start`/reconnect call with `isDisposedOrDisposing()` before invoking it.
  2. Avoid calling `start()` more than once — it is idempotent via `_startPromise`, so reuse the existing promise instead of re-driving startup.
  3. Cancel pending reconnect timers in your `dispose`/`onCircuitClosed` handler so they cannot fire after disposal.
  4. For bfcache, listen for `pageshow` with `persisted` and do not re-start a disposed circuit; let a fresh page load build a new one.

Example fix

// before
async function tryRestart(cm: CircuitManager) {
  await cm.start(); // throws if already disposing
}

// after
async function tryRestart(cm: CircuitManager) {
  if (cm.isDisposedOrDisposing()) return;
  await cm.start();
}
Defensive patterns

Strategy: validation

Validate before calling

function safeStart(cm: CircuitManager) {
  return cm.isDisposedOrDisposing() ? Promise.resolve(false) : cm.start();
}

Try / catch

try { await cm.start(); }
catch (e) { if (/disposed circuit/.test(e.message)) { /* circuit gone, do not restart */ return; } throw e; }

Prevention

When it happens

Trigger: Calling `circuitManager.start()` (directly or via a re-entrant `Blazor.start`/reconnect path) after `dispose()` has begun — `_disposePromise` is defined, so `isDisposedOrDisposing()` returns true.

Common situations: A tab being closed/unloaded while a manual reconnect or late `start()` is queued; SPA fast-back/forward caches (bfcache) replaying startup after the page already tore down; custom reconnection logic calling `start` in a retry loop without checking disposal; hot reload or render-mode switching disposing the old circuit while a deferred start runs.

Related errors


AI-assisted analysis of dotnet/aspnetcore@3600ca084e (2026-08-11). Data as JSON: /api/errors/906c5e7161c566a3. Report an issue: GitHub.

Appendix: source

Thrown at src/Components/Web.JS/src/Platform/Circuits/CircuitManager.ts:88

    componentManager: RootComponentManager<ServerComponentDescriptor>,
    appState: string,
    options: CircuitStartOptions,
    logger: ConsoleLogger,
    eventRegistry: JSEventRegistry,
  ) {
    this._circuitId = undefined;
    this._applicationState = appState;
    this._componentManager = componentManager;
    this._options = options;
    this._logger = logger;
    this._eventRegistry = eventRegistry;
    this._renderQueue = new RenderQueue(this._logger);
    this._dispatcher = DotNet.attachDispatcher(this);
  }

  public start(): Promise<boolean> {
    if (this.isDisposedOrDisposing()) {
      throw new Error('Cannot start a disposed circuit.');
    }

    if (!this._startPromise) {
      this._startPromise = this.startCore();
    }

    return this._startPromise;
  }

  public updateRootComponents(operations: string, serverState: string): Promise<void> | undefined {
    if (this._isFirstRender) {
      this._isFirstRender = false;
      return this._connection?.send('UpdateRootComponents', operations, this._applicationState);
    } else {
      return this._connection?.send('UpdateRootComponents', operations, serverState);
    }
  }

View on GitHub (pinned to 3600ca084e)