{"record":{"id":"5f2b71e929585aeb","repo":"microsoft/typescript-go","slug":"w-symbol-handle-d-not-found-in-snapshot-registr","errorCode":null,"errorMessage":"%w: symbol handle %d not found in snapshot registry","messagePattern":"%w: symbol handle (.+?) not found in snapshot registry","errorType":"validation","errorClass":"ErrClientError","httpStatus":null,"severity":"error","filePath":"internal/api/session.go","lineNumber":246,"sourceCode":"\t\t}\n\t\treturn id\n\t}\n\treg.typeRegistry[id] = t\n\treturn id\n}\n\n// resolveSymbolHandle resolves a symbol handle within the snapshot's registry.\nfunc (sd *snapshotData) resolveSymbolHandle(handle SymbolID) (*ast.Symbol, error) {\n\tif handle == 0 {\n\t\treturn nil, fmt.Errorf(\"%w: empty symbol handle\", ErrClientError)\n\t}\n\n\tsd.symbolRegistryMu.RLock()\n\tsymbol, ok := sd.symbolRegistry[handle]\n\tsd.symbolRegistryMu.RUnlock()\n\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"%w: symbol handle %d not found in snapshot registry\", ErrClientError, handle)\n\t}\n\n\treturn symbol, nil\n}\n\n// resolveTypeHandle resolves a type handle within the project's registry.\nfunc (sd *snapshotData) resolveTypeHandle(projectID ProjectID, handle TypeID) (*checker.Type, error) {\n\tif handle == 0 {\n\t\treturn nil, fmt.Errorf(\"%w: empty type handle\", ErrClientError)\n\t}\n\tif projectID == \"\" {\n\t\treturn nil, fmt.Errorf(\"%w: empty project ID for type handle %d\", ErrClientError, handle)\n\t}\n\n\tsd.projectRegistriesMu.RLock()\n\treg := sd.projectRegistries[projectID]\n\tsd.projectRegistriesMu.RUnlock()\n","sourceCodeStart":228,"sourceCodeEnd":264,"githubUrl":"https://github.com/microsoft/typescript-go/blob/1bcfa18d79a3be41772223d5c05dfe4480e614ff/internal/api/session.go#L228-L264","documentation":"The SymbolID is non-zero but absent from this snapshot's symbolRegistry, so resolveSymbolHandle fails with \"symbol handle N not found in snapshot registry\". Symbols are registered snapshot-wide (ids come from a global counter, so the same symbol keeps one id), but the registry itself lives and dies with the snapshotData: once that snapshot is released or replaced, all its symbol handles become unresolvable.","triggerScenarios":"Using a SymbolID obtained from snapshot A in a request against snapshot B (before or after an updateSnapshot); calling release for the snapshot and then re-using its handles; ids carried over after a server restart (the counter and registry reset); ids from a different session/process; fabricated ids.","commonSituations":"Client caches symbols in editor decorations/hover state across document edits (each updateSnapshot mints a new snapshot); long-lived background analyzers holding symbol ids; session restart invalidating every stored handle.","solutions":["Re-acquire the symbol in the current snapshot (e.g. getSymbolAtPosition/getSymbolAtLocation) instead of reusing an old handle","Invalidate all cached symbol handles whenever updateSnapshot returns a new snapshot id or release completes","Keep handles paired with the snapshot id they came from and always send that snapshot in follow-ups"],"exampleFix":"// before\nconst symId = cache.get(\"hoverSymbol\"); // from snapshot N-1\nawait call(\"getMembersOfSymbol\", { snapshot: current, project, symbol: symId }); // not found\n\n// after\nconst sym = await call(\"getSymbolAtPosition\", { snapshot: current, project, file, position });\nif (sym) await call(\"getMembersOfSymbol\", { snapshot: current, project, symbol: sym.id });","handlingStrategy":"fallback","validationCode":"// TS client: never send a symbol handle from another snapshot.\nfunction symbolUsableIn(handle: {snapshot: bigint, id: number}, current: bigint): boolean {\n  return handle.snapshot === current;\n}\nif (!symbolUsableIn(cached, snapshot)) {\n  cached = await requerySymbolAtPosition(file, position); // refresh in current snapshot\n}","typeGuard":"type SymbolRef = { snapshot: bigint; project: string; id: number };\nconst isResolvableSymbolRef = (r: SymbolRef | null | undefined, current: bigint): r is SymbolRef =>\n  !!r && typeof r.id === \"number\" && r.id > 0 && r.snapshot === current;","tryCatchPattern":"try {\n  return await call(method, params);\n} catch (e) {\n  if (String(e).includes(\"not found in snapshot registry\")) {\n    // handle is stale for this snapshot: re-acquire the symbol at its location and retry once\n    const sym = await call(\"getSymbolAtPosition\", { snapshot: params.snapshot, project: params.project, file, position });\n    if (sym) return await call(method, { ...params, symbol: sym.id });\n  }\n  throw e;\n}","preventionTips":["Store {snapshot, project, id} together for every symbol you keep","Drop all symbol caches when updateSnapshot returns a new id or release completes","Re-acquire symbols at their source location instead of trusting long-lived ids in editor state"],"tags":["symbol-handle","registry","snapshot","stale-handle","lifecycle","client-error"],"backgroundTag":null,"analyzedSha":"1bcfa18d79a3be41772223d5c05dfe4480e614ff","analyzedAt":"2026-08-16T02:12:00.115Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}