dotnet/runtime · error · Error

startup diagnostic client already registered

Error message

startup diagnostic client already registered

What it means

setupJsClient() with startup=true throws if startupJsClient is already set. Only one startup diagnostic client is allowed because the diagnostic server reserves a single slot for the client that handles the very first advert (the runtime's banner). Registering twice would orphan the first client.

Solutions

  1. Register exactly one startup client; use the scenario-name switch in createDiagConnectionJs to multiplex.
  2. If you need multiple collection types, register one startup client and dispatch inside its callbacks.
  3. For ad-hoc collection after boot, call collectMetrics() without startup=true.
  4. Guard: only call setupJsClient(client, true) once per page load.

Example fix

// before: two startup clients
collectMetrics({}, true);
collectCpuSamples({}, true); // throws

// after: only one startup, switch later
collectMetrics({}, true); // startup slot taken
// later, after runtime is up:
collectCpuSamples(); // runtime-time collection
Defensive patterns

Strategy: validation

Validate before calling

let startupClientRegistered = false;
function registerStartupSafely(client: IDiagnosticClient) {
  if (startupClientRegistered) throw new Error('Startup client already set');
  setupJsClient(client, true);
  startupClientRegistered = true;
}

Type guard

function isStartupSlotFree(): boolean {
  // Reflect on the diagnostic-server-js module state via the loader's exports
  return !(globalThis as any).__diagStartupClientRegistered;
}

Try / catch

try { collectMetrics({}, true); } catch (e) {
  if (/already registered/.test((e as Error).message)) {
    // a startup client exists; use runtime-time collection instead
    await dotnet.create();
    collectMetrics();
  } else throw e;
}

Prevention

When it happens

Trigger: Calling collectMetrics({}, true) and then collectCpuSamples({}, true) — both try to register as the startup client. Calling createDiagConnectionJs with multiple scenario names that each register. A library and the app both calling setupJsClient(client, true).

Common situations: Two diagnostic modules (counters + cpu-profiler) both initialized at startup. Plugin architecture where each plugin registers its own startup client. Re-invoking collect* with startup=true after a hot reload.

Related errors


AI-assisted analysis of dotnet/runtime@60108ba66e (2026-08-10). Data as JSON: /api/errors/029af87324a485d8. Report an issue: GitHub.

Appendix: source

Thrown at src/native/libs/System.Native.Browser/diagnostics/diagnostic-server-js.ts:154

            downloadBlob(this.messagesToSend);
        }
        this.messagesToSend = [];
        return 0;
    }
}

export function initializeJsClient() {
    nextJsClient = dotnetLoaderExports.createPromiseCompletionSource<IDiagnosticClient>();
    startupJsClient = undefined;
}

export function setupJsClient(client: IDiagnosticClient, startup?: boolean) {
    if (!startup && !dotnetLoaderExports.isRuntimeRunning()) {
        throw new Error("Runtime is not running");
    }
    if (startup) {
        if (startupJsClient) {
            throw new Error("startup diagnostic client already registered");
        }
        startupJsClient = client;
    } else {
        if (nextJsClient.isDone) {
            throw new Error("multiple clients in parallel are not allowed");
        }
        nextJsClient.resolve(client);
    }
}

export function createDiagConnectionJs(socketHandle: number, scenarioName: string): DiagnosticSession {
    if (!fromScenarioNameOnce) {
        fromScenarioNameOnce = true;
        if (scenarioName.startsWith("js://gcdump")) {
            collectGcDump({}, true);
        }
        if (scenarioName.startsWith("js://counters")) {
            collectMetrics({}, true);

View on GitHub (pinned to 60108ba66e)