{"record":{"id":"8c13d06980115406","repo":"actualbudget/actual","slug":"internal-error-category-group-with-id-targetgro","errorCode":null,"errorMessage":"Internal error: category group with ID ${targetGroupId} 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":115,"sourceCode":"        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(\n            `Internal error: category group with ID ${targetGroupId} not found.`,\n          );\n        }\n\n        const nextToTargetCategory = categoryGroups[targetGroupIndex + 1];\n\n        moveCategoryGroup.mutate({\n          id: groupToMove.id,\n          // Due to the way `moveCategory` works, we use the category next to the\n          // actual target category here because `moveCategory` always shoves the\n          // category *before* the target category.\n          // On the other hand, using `null` as `targetId` moves the category\n          // to the end of the list.\n          targetId: nextToTargetCategory?.id || null,\n        });\n      }\n    },\n  });","sourceCodeStart":97,"sourceCodeEnd":133,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/desktop-client/src/components/mobile/budget/ExpenseGroupList.tsx#L97-L133","documentation":"ExpenseGroupList throws when an 'after'-position drop references a target group ID not present in `categoryGroups`. The invariant protects the subsequent `categoryGroups[targetGroupIndex + 1]` access used to insert the moved group.","triggerScenarios":"dropPosition === 'after' with `categoryGroups.findIndex(c => c.id === targetGroupId)` returning -1: the drop target key is stale, from a collapsed/hidden group, or removed concurrently.","commonSituations":"Target group deleted on another device mid-drag; drop landed on a synthetic/hidden row whose key is not a real group ID; list refreshed between drag start and drop.","solutions":["Refresh categoryGroups and retry the drag operation.","Ensure drop targets only render keys of groups present in categoryGroups.","No-op when the target index is -1 instead of throwing.","Investigate sync-driven deletions if it reproduces."],"exampleFix":"// before\nif (targetGroupIndex === -1) {\n  throw new Error(`Internal error: category group with ID ${targetGroupId} not found.`);\n}\n// after\nif (targetGroupIndex === -1) {\n  console.warn(`Drop target ${targetGroupId} no longer exists; ignoring`);\n  return;\n}","handlingStrategy":"validation","validationCode":"const targetGroupIndex = categoryGroups.findIndex(c => c.id === targetGroupId);\nif (e.target.dropPosition === 'after' && targetGroupIndex === -1) {\n  return; // stale target; skip\n}","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":["Verify drop target IDs exist in the live groups array before index math.","Exclude hidden/collapsed synthetic rows from drop targets.","Refresh state on sync deletions affecting groups.","Log stale drops instead of 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"}