dotnet/aspnetcore · error · Error

Interop methods are already registered for renderer

Error message

Interop methods are already registered for renderer ${rendererId}

What it means

Thrown by attachWebWidgetInterop when interopMethodsByRenderer already has an entry for the given rendererId. Each renderer should attach its interop bridge exactly once; a second attachment indicates a double-initialization bug. The map is keyed by rendererId, so the same id registering twice is the trigger.

Solutions

  1. Call detachWebRendererInterop(rendererId) before re-attaching for the same id.
  2. Ensure Blazor.start is invoked exactly once per page.
  3. Avoid sharing a rendererId across multiple renderer instances; let the framework allocate ids.
  4. In reconnect logic, follow the framework's detach->attach sequence rather than attach->attach.

Example fix

// before (throws on second call)
attachWebRendererInterop(1, interop, params, inits);
attachWebRendererInterop(1, interop2, params, inits);

// after
detachWebRendererInterop(1);
attachWebRendererInterop(1, interop2, params, inits);
Defensive patterns

Strategy: validation

Validate before calling

if (interopMethodsByRenderer.has(rendererId)) {
  // already attached — detach first if this is a legitimate re-attach
  detachWebRendererInterop(rendererId);
}
attachWebRendererInterop(rendererId, interop, params, inits);

Prevention

When it happens

Trigger: Blazor.start being called twice; a circuit reconnect path that re-attaches without first detaching; two interactive renderers created with the same id; a misconfigured custom host that calls attachWebRendererInterop manually more than once.

Common situations: Hot reload re-running initialization; a custom hosting layer that manually drives Blazor lifecycle and calls attach twice; testing harness that doesn't tear down between runs.

Related errors


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

Appendix: source

Thrown at src/Components/Web.JS/src/Rendering/WebRendererInteropMethods.ts:27

const rendererAttachedListeners: ((browserRendererId: number) => void)[] = [];
const rendererByIdResolverMap: Map<number, [() => void | undefined, Promise<void> | undefined]> = new Map();

export function attachRendererIdResolver(rendererId: number, resolver: () => void | undefined, promise: Promise<void> | undefined) {
  rendererByIdResolverMap.set(rendererId, [resolver, promise]);
}

export function getRendererAttachedPromise(rendererId: number): Promise<void> | undefined {
  return rendererByIdResolverMap.get(rendererId)?.[1];
}

export function attachWebRendererInterop(
  rendererId: number,
  interopMethods: DotNet.DotNetObject,
  jsComponentParameters?: JSComponentParametersByIdentifier,
  jsComponentInitializers?: JSComponentIdentifiersByInitializer,
): void {
  if (interopMethodsByRenderer.has(rendererId)) {
    throw new Error(`Interop methods are already registered for renderer ${rendererId}`);
  }

  interopMethodsByRenderer.set(rendererId, interopMethods);

  if (jsComponentParameters && jsComponentInitializers && Object.keys(jsComponentParameters).length > 0) {
    const manager = getInteropMethods(rendererId);
    enableJSRootComponents(rendererId, manager, jsComponentParameters, jsComponentInitializers);
  }

  rendererByIdResolverMap.get(rendererId)?.[0]?.();

  invokeRendererAttachedListeners(rendererId);
}

export function detachWebRendererInterop(rendererId: number): DotNet.DotNetObject {
  const interopMethods = interopMethodsByRenderer.get(rendererId);
  if (!interopMethods) {
    throw new Error(`Interop methods are not registered for renderer ${rendererId}`);

View on GitHub (pinned to 3600ca084e)