gitbutlerapp/gitbutler · error

Could not find selected project

Error message

Could not find selected project

What it means

useHunkMenuItems looks up the project matching the projectId route param from the cached projects list and throws synchronously when none matches. This is an internal invariant: the hook's menu-item construction cannot proceed without a valid project. It guards against building hunk actions (commit/discard/uncommit) against a nonexistent project.

Solutions

  1. Verify the projectId route param matches an existing project id in the projects list (log both to compare).
  2. Gate the hook/route behind the list-projects query's isLoading state so it only runs once projects are loaded.
  3. If the project was deleted, navigate the user back to the project list instead of rendering the workspace route.
  4. Refresh the projects list (invalidate listProjectsQueryOptions) if another client may have changed it.

Example fix

// before
const selectedProject = projects.find((project) => project.id === projectId);
if (!selectedProject) throw new Error("Could not find selected project");

// after
const selectedProject = projects.find((project) => project.id === projectId);
useEffect(() => {
  if (!selectedProject && !isProjectsLoading) {
    navigate({ to: "/" }); // leave the stale project route
  }
}, [selectedProject, isProjectsLoading]);
Defensive patterns

Strategy: validation

Validate before calling

const projects = useQuery(listProjectsQueryOptions).data ?? [];
const canRender = projects.some((p) => p.id === projectId);

Type guard

const hasProject = (projects: Project[], id: string): projects is [Project, ...Project[]] & Project[] =>
  projects.some((p) => p.id === id);

Try / catch

try {
  const items = buildHunkMenuItems(projectId, ...);
} catch (e) {
  if (e instanceof Error && e.message === "Could not find selected project") {
    navigate({ to: "/" });
  } else throw e;
}

Prevention

When it happens

Trigger: Calling useHunkMenuItems(projectId) while the projects query cache does not contain a project whose id equals projectId — e.g. the project was removed, the list query hasn't resolved yet, or the route id is stale/invalid.

Common situations: A project is deleted in another window/instance while a workspace route for it is open; deep-linking to /project/<bad-id>/workspace; the projects list query failing or still loading so the cache is empty; using a stale bookmarked id after re-cloning.

Understand the failure class

Background: "Not found" and "does not exist" errors: why "Task not found", "No such folder", and "Can't find" fire when a lookup comes back empty — this error's family across 14 libraries.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@58e5313667 (2026-09-18). Data as JSON: /api/errors/aa4272384a213bb6. Report an issue: GitHub.

Appendix: source

Thrown at apps/lite/ui/src/routes/project/$id/workspace/useHunkMenuItems.ts:54

	checkedProbe: Extract<Address, { _tag: "Hunk" }> | null;
	usesSelectedLines: boolean;
};

export const useHunkMenuItems = ({
	projectId,
}: {
	projectId: string;
}): ((target: HunkMenuTarget) => Array<NativeMenuItem>) => {
	const store = useAppStore();
	const { data: projects } = useSuspenseQuery(listProjectsQueryOptions);
	const { data: editors } = useQuery(listEditorsQueryOptions);
	const { data: preferredEditor } = useQuery({
		...guiSettingsQueryOptions,
		select: (cfg) => editors?.find((editor) => editor.id === cfg.editorId),
	});

	const selectedProject = projects.find((project) => project.id === projectId);
	if (!selectedProject) throw new Error("Could not find selected project");

	const { isPending: isCommitUncommitChangesPending, mutate: commitUncommitChanges } =
		useCommitUncommitChanges();
	const { isPending: isCommitDiscardChangesPending, mutate: commitDiscardChanges } =
		useCommitDiscardChanges();
	const { isPending: isDiscardWorktreeChangesPending, mutate: discardWorktreeChanges } =
		useDiscardWorktreeChanges();
	const { isPending: isOpenInProgramPending, mutate: openInProgram } = useOpenInProgram();
	const revealInFolder = useRevealInFolder(projectId);

	return ({ sources, checkedProbe, usesSelectedLines, change, hunk, lineNumber }) => {
		const state = store.getState();
		const usesCheckedLines =
			checkedProbe !== null &&
			projectSlice.selectors.selectAddressChecked(state, projectId, checkedProbe);
		const cutSources = usesCheckedLines
			? projectSlice.selectors.selectCheckedAddresses(state, projectId)
			: sources;

View on GitHub (pinned to 58e5313667)