dotnet/aspnetcore · critical · Error

Received persisted state for circuit ID '${circuitId}', but

Error message

Received persisted state for circuit ID '${circuitId}', but the current circuit ID is '${this._circuitId}'.

What it means

Thrown in the 'JS.SavePersistedState' handler when the circuitId the server references does not equal the client's current _circuitId. Persisted state is circuit-scoped, so accepting it for a different circuit would corrupt state; the client refuses the mismatch.

Source

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

        this.changeActivity(1);
      }
      (this._dispatcher.beginInvokeJSFromDotNet as (...a: unknown[]) => unknown)
        .call(this._dispatcher, asyncHandle, ...rest);
    });
    connection.on('JS.EndInvokeDotNet', (asyncCallId: string, success: boolean, resultJsonOrExceptionMessage: string) => {
      if (asyncCallId) {
        this.changeActivity(-1);
      }
      this._dispatcher.endInvokeDotNetFromJS(asyncCallId, success, resultJsonOrExceptionMessage);
    });
    connection.on('JS.ReceiveByteArray', this._dispatcher.receiveByteArray.bind(this._dispatcher));

    connection.on('JS.SavePersistedState', (circuitId: string, components: string, applicationState: string) => {
      if (!this._circuitId) {
        throw new Error('Circuit host not initialized.');
      }
      if (circuitId !== this._circuitId) {
        throw new Error(`Received persisted state for circuit ID '${circuitId}', but the current circuit ID is '${this._circuitId}'.`);
      }
      this._persistedCircuitState = { components, applicationState };
      return true;
    });

    connection.on('JS.BeginTransmitStream', (streamId: number) => {
      const readableStream = new ReadableStream({
        start: (controller) => {
          this.changeActivity(1);
          connection.stream('SendDotNetStreamToJS', streamId).subscribe({
            next: (chunk: Uint8Array) => controller.enqueue(chunk),
            complete: () => { controller.close(); this.changeActivity(-1); },
            error: (err) => { controller.error(err); this.changeActivity(-1); },
          });
        },
      });

      this._dispatcher.supplyDotNetStream(streamId, readableStream);

View on GitHub (pinned to 294cab2f9b)

Solutions

  1. Stabilize the connection (network, proxy, WebSocket availability) so only one circuit lifecycle is active.
  2. Match client and server framework versions to keep the persisted-state protocol consistent.
  3. Avoid programmatically forcing reconnects in parallel with an in-flight resume; let the framework serialize them.
Defensive patterns

Strategy: try-catch

Try / catch

// The framework throws internally; surface a clean reconnect UI.
try { await circuitManager.resume(); }
catch (e) {
  if (/current circuit ID/i.test(e.message)) {
    showReconnectUI(); // prompt reload
  } else throw e;
}

Prevention

When it happens

Trigger: A reconnection/restart assigned a new circuitId while a SavePersistedState message for the old circuit was still in flight; concurrent or racing circuit handshakes; server-side circuit reuse/rotation during a paused session.

Common situations: Unstable networks causing multiple rapid reconnects; tab freeze/thaw triggering pause-resume where the server rotated the circuit; version skew between client and server persistence protocols.

Related errors


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