{"record":{"id":"3e3d0ff6e1c26f52","repo":"actualbudget/actual","slug":"internal-error-category-group-with-id-groupidto","errorCode":null,"errorMessage":"Internal error: category group with ID ${groupIdToMove} not found.","messagePattern":"Internal error: category group with ID (.+?) not found\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/desktop-client/src/components/mobile/budget/ExpenseGroupList.tsx","lineNumber":97,"sourceCode":"        <ExpenseGroupHeader\n          categoryGroup={group}\n          month={month}\n          showBudgetedColumn={showBudgetedColumn}\n          show3Columns={show3Columns}\n          onEditCategoryGroup={() => {}}\n          isCollapsed={() => true}\n          onToggleCollapse={() => {}}\n          isHidden={false}\n        />\n      );\n    },\n    onReorder: e => {\n      const [key] = e.keys;\n      const groupIdToMove = key as CategoryGroupEntity['id'];\n      const groupToMove = categoryGroups.find(c => c.id === groupIdToMove);\n\n      if (!groupToMove) {\n        throw new Error(\n          `Internal error: category group with ID ${groupIdToMove} not found.`,\n        );\n      }\n\n      const targetGroupId = e.target.key as CategoryEntity['id'];\n\n      if (e.target.dropPosition === 'before') {\n        moveCategoryGroup.mutate({\n          id: groupToMove.id,\n          targetId: targetGroupId,\n        });\n      } else if (e.target.dropPosition === 'after') {\n        const targetGroupIndex = categoryGroups.findIndex(\n          c => c.id === targetGroupId,\n        );\n\n        if (targetGroupIndex === -1) {\n          throw new Error(","sourceCodeStart":79,"sourceCodeEnd":115,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/desktop-client/src/components/mobile/budget/ExpenseGroupList.tsx#L79-L115","documentation":"ExpenseGroupList's onReorder handler throws when the dragged group ID (`e.keys[0]`) cannot be found in `categoryGroups`. The invariant ensures the group being moved actually exists before computing its insertion position.","triggerScenarios":"`onReorder` fires with a key whose `categoryGroups.find(c => c.id === groupIdToMove)` returns undefined — group deleted concurrently, list filtered, or drag payload from a stale render.","commonSituations":"Group removed via sync/another tab mid-drag; dragging a group header whose list was re-rendered with fresh IDs; drag started before a budget-month switch replaced the groups.","solutions":["Refresh categoryGroups from the store and retry the reorder.","Bind the drag source keys to the same array passed to onReorder.","Return early (no-op) when the group lookup misses instead of throwing.","Audit for concurrent group deletions during active drags."],"exampleFix":"// before\nif (!groupToMove) {\n  throw new Error(`Internal error: category group with ID ${groupIdToMove} not found.`);\n}\n// after\nif (!groupToMove) {\n  console.warn(`Skipping reorder: group ${groupIdToMove} no longer exists`);\n  return;\n}","handlingStrategy":"validation","validationCode":"const [key] = e.keys;\nconst groupToMove = categoryGroups.find(c => c.id === key);\nif (!groupToMove) return; // stale key; skip reorder","typeGuard":"function groupExists(groups: { id: string }[], id: string): boolean {\n  return groups.some(g => g.id === id);\n}","tryCatchPattern":"try {\n  onReorder(e);\n} catch (err) {\n  if (String(err).includes('not found')) {\n    refreshCategoryGroups();\n  } else {\n    throw err;\n  }\n}","preventionTips":["Validate drag keys against current categoryGroups before reordering.","Refresh group state after budget month switches or sync events.","Bind the drag source to the same array consumed by onReorder.","Prefer no-op handling for stale drags over throwing."],"tags":["ui","drag-and-drop","state-desync","internal-error"],"backgroundTag":"stale-drop-target-id","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}