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
- Register exactly one startup client; use the scenario-name switch in createDiagConnectionJs to multiplex.
- If you need multiple collection types, register one startup client and dispatch inside its callbacks.
- For ad-hoc collection after boot, call collectMetrics() without startup=true.
- 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
- Designate exactly one startup client in your app architecture.
- Track registration in a module-level boolean and check before setupJsClient(client, true).
- Document the startup-client contract for plugin authors.
- Use createDiagConnectionJs scenario names to multiplex collection types.
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
- Diag module already loaded
- multiple clients in parallel are not allowed
- Native module already loaded
- No active JS diagnostic session
- No active JS diagnostic session
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)