microsoft/typescript-go · error · ErrClientError

%w: project %s not found

Error message

%w: project %s not found

What it means

The snapshot's ProjectCollection has no project at the path given by the ProjectID handle (a ProjectID is the project's path string; parseProjectHandle extracts it and GetProjectByPath returns nil). Every project-scoped request fails with "api: client error: project <path> not found". Project membership is per-snapshot: a project handle is only valid inside the snapshot whose updateSnapshot produced it.

Source

Thrown at internal/api/session.go:97

	proj, err := sd.getProject(projectHandle)
	if err != nil {
		return nil, err
	}

	program := proj.GetProgram()
	if program == nil {
		return nil, fmt.Errorf("%w: project has no program", ErrClientError)
	}

	return program, nil
}

// getProject looks up a project from a project handle within this snapshot.
func (sd *snapshotData) getProject(projectHandle ProjectID) (*project.Project, error) {
	projectName := parseProjectHandle(projectHandle)
	proj := sd.snapshot.ProjectCollection.GetProjectByPath(projectName)
	if proj == nil {
		return nil, fmt.Errorf("%w: project %s not found", ErrClientError, projectName)
	}
	return proj, nil
}

// nodeHandleFrom creates an index-based node handle (index.kind.path), building a node index table
// for the file on-demand if needed.
func (sd *snapshotData) nodeHandleFrom(node *ast.Node) NodeHandle {
	sourceFile := ast.GetSourceFileOfNode(node)
	path := sourceFile.Path()
	table := encoder.GetNodeIndexTable(sourceFile)
	idx := table.GetIndex(node)
	return NodeHandle(fmt.Sprintf("%d.%d.%s", idx, node.Kind, path))
}

// getOrCreateProjectRegistry returns the registry for the given project, creating it if needed.
func (sd *snapshotData) getOrCreateProjectRegistry(projectID ProjectID) *projectRegistryData {
	if projectID == "" {
		panic("getOrCreateProjectRegistry: empty project ID")

View on GitHub (pinned to 1bcfa18d79)

Solutions

  1. Use only handles the server returned for the current snapshot: getDefaultProjectForFile, initialize/updateSnapshot responses, or the Project field on SymbolResponse
  2. If the project is missing, open its files via updateSnapshot so the project materializes, then re-fetch the handle
  3. Scope cached handles: key them by (session, snapshot) and invalidate on every updateSnapshot
  4. Never synthesize a ProjectID client-side from a filesystem path

Example fix

// before
const cached = localStorage.get("projectHandle"); // handle from an older snapshot
await call("getTypeOfSymbol", { snapshot, project: cached, symbol }); // project not found

// after
const { project } = await call("getDefaultProjectForFile", { snapshot, file });
await call("getTypeOfSymbol", { snapshot, project, symbol });
Defensive patterns

Strategy: validation

Validate before calling

// TS client: keep the set of live (snapshot -> projects) and validate before sending.
function assertKnownProject(snapshot: bigint, project: string) {
  const known = liveProjects.get(snapshot);
  if (!known || !known.has(project)) {
    throw new Error(`project ${project} not known in snapshot ${snapshot}; refresh via getDefaultProjectForFile/updateSnapshot`);
  }
}
// record: liveProjects.get(resp.snapshot)?.add(resp.project) on every project-bearing response

Type guard

const isLiveProjectHandle = (p: string | undefined | null, snapshot: bigint, known: Set<string>): p is string =>
  typeof p === "string" && p.length > 0 && known.has(`${snapshot}:${p}`);

Try / catch

try {
  return await call(method, params);
} catch (e) {
  if (String(e).includes("project ") && String(e).includes(" not found")) {
    const { project } = await call("getDefaultProjectForFile", { snapshot: params.snapshot, file });
    return await call(method, { ...params, project }); // retry once with a server-issued handle
  }
  throw e;
}

Prevention

When it happens

Trigger: Passing a ProjectID obtained from a previous snapshot after updateSnapshot rebuilt the project set; passing a hand-written tsconfig path instead of a server-returned handle; querying a project whose files were never opened in this session (projects materialize from open files); mixing handles across two concurrent sessions/processes.

Common situations: Client caches project handles across snapshot updates; editor tooling opening a new file set that drops a project; splitting a monorepo query across sessions; assuming ProjectID equals the tsconfig.json path the client computed itself.

Related errors


AI-assisted analysis of microsoft/typescript-go@1bcfa18d79 (2026-08-16). Data as JSON: /api/errors/feacc2f37d9e82ca. Report an issue: GitHub.