{"record":{"id":"baedc461d3927a6d","repo":"dotnet/aspnetcore","slug":"interop-methods-are-not-registered-for-renderer","errorCode":null,"errorMessage":"Interop methods are not registered for renderer ${rendererId}","messagePattern":"Interop methods are not registered for renderer (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/Components/Web.JS/src/Rendering/WebRendererInteropMethods.ts","lineNumber":45,"sourceCode":"    throw new Error(`Interop methods are already registered for renderer ${rendererId}`);\n  }\n\n  interopMethodsByRenderer.set(rendererId, interopMethods);\n\n  if (jsComponentParameters && jsComponentInitializers && Object.keys(jsComponentParameters).length > 0) {\n    const manager = getInteropMethods(rendererId);\n    enableJSRootComponents(rendererId, manager, jsComponentParameters, jsComponentInitializers);\n  }\n\n  rendererByIdResolverMap.get(rendererId)?.[0]?.();\n\n  invokeRendererAttachedListeners(rendererId);\n}\n\nexport function detachWebRendererInterop(rendererId: number): DotNet.DotNetObject {\n  const interopMethods = interopMethodsByRenderer.get(rendererId);\n  if (!interopMethods) {\n    throw new Error(`Interop methods are not registered for renderer ${rendererId}`);\n  }\n\n  interopMethodsByRenderer.delete(rendererId);\n  return interopMethods;\n}\n\nexport function isRendererAttached(browserRendererId: number): boolean {\n  return interopMethodsByRenderer.has(browserRendererId);\n}\n\nexport function registerRendererAttachedListener(listener: (browserRendererId: number) => void) {\n  rendererAttachedListeners.push(listener);\n}\n\nfunction invokeRendererAttachedListeners(browserRendererId: number) {\n  for (const listener of rendererAttachedListeners) {\n    listener(browserRendererId);\n  }","sourceCodeStart":27,"sourceCodeEnd":63,"githubUrl":"https://github.com/dotnet/aspnetcore/blob/294cab2f9b2e03af6b953820c7ab497c3c8b7ad9/src/Components/Web.JS/src/Rendering/WebRendererInteropMethods.ts#L27-L63","documentation":"Thrown by detachWebRendererInterop when you try to detach (dispose) a renderer whose interop object was never registered, or was already detached. The function reads interopMethodsByRenderer.get(rendererId) and, on a miss, throws. Blazor uses this to keep its renderer-interop map consistent; calling detach twice or detaching an unregistered renderer breaks that invariant.","triggerScenarios":"Calling detachWebRendererInterop(rendererId) for a rendererId that was never passed to attachWebRendererInterop, or calling it a second time after a successful detach (the map entry was deleted on the first detach).","commonSituations":"Manual teardown/disposal logic that double-disposes a renderer; a custom host that tears down renderers out of order; a circuit-handler that calls detach during both normal disconnect and app shutdown; test harnesses that re-initialize Blazor without resetting module-level state.","solutions":["Guard the detach call with isRendererAttached(rendererId) before invoking detachWebRendererInterop.","Ensure detach is called exactly once per attach by tracking attach/detach pairing in your own state machine.","If re-initializing Blazor in a SPA without a full reload, perform a full location.reload() instead of partial teardown, since these maps are module-level singletons.","Search your codebase for all detachWebRendererInterop call sites and confirm each has a matching attach."],"exampleFix":"// before\nconst interop = detachWebRendererInterop(rendererId);\n\n// after\nif (isRendererAttached(rendererId)) {\n  const interop = detachWebRendererInterop(rendererId);\n  interop.dispose();\n}","handlingStrategy":"validation","validationCode":"import { isRendererAttached } from './WebRendererInteropMethods';\n\nfunction safeDetach(rendererId: number) {\n  if (!isRendererAttached(rendererId)) {\n    return null; // nothing to detach\n  }\n  return detachWebRendererInterop(rendererId);\n}","typeGuard":"function isDetachable(rendererId: number): rendererId is number {\n  return isRendererAttached(rendererId);\n}","tryCatchPattern":"try {\n  const interop = detachWebRendererInterop(rendererId);\n  interop.dispose();\n} catch (e) {\n  if (/not registered for renderer/.test((e as Error).message)) {\n    // already detached or never attached — idempotent, ignore\n  } else { throw e; }\n}","preventionTips":["Track attach/detach pairing in a single owner so detach runs exactly once.","Always guard detach with isRendererAttached(rendererId).","Prefer a full page reload over partial Blazor teardown in long-lived SPAs."],"tags":["blazor","renderer","interop","lifecycle","double-dispose"],"analyzedSha":"294cab2f9b2e03af6b953820c7ab497c3c8b7ad9","analyzedAt":"2026-08-06T20:08:02.189Z","schemaVersion":2},"datasetVersion":"2026-08-06T23:17:07.152Z"}