{"record":{"id":"06c57974d5bf1af1","repo":"actualbudget/actual","slug":"internal-error-category-with-id-categoryidtomov-06c579","errorCode":null,"errorMessage":"Internal error: category with ID ${categoryIdToMove} not found.","messagePattern":"Internal error: category with ID (.+?) not found\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/desktop-client/src/components/mobile/budget/IncomeCategoryList.tsx","lineNumber":58,"sourceCode":"          target={target}\n          className={css({\n            '&[data-drop-target]': {\n              height: 4,\n              backgroundColor: theme.tableBorderSeparator,\n              opacity: 1,\n              borderRadius: 4,\n            },\n          })}\n        />\n      );\n    },\n    onReorder: e => {\n      const [key] = e.keys;\n      const categoryIdToMove = key as CategoryEntity['id'];\n      const categoryToMove = categories.find(c => c.id === categoryIdToMove);\n\n      if (!categoryToMove) {\n        throw new Error(\n          `Internal error: category with ID ${categoryIdToMove} not found.`,\n        );\n      }\n\n      if (!categoryToMove.group) {\n        throw new Error(\n          `Internal error: category ${categoryIdToMove} is not in a group and cannot be moved.`,\n        );\n      }\n\n      const targetCategoryId = e.target.key as CategoryEntity['id'];\n\n      if (e.target.dropPosition === 'before') {\n        moveCategory.mutate({\n          id: categoryToMove.id,\n          groupId: categoryToMove.group,\n          targetId: targetCategoryId,\n        });","sourceCodeStart":40,"sourceCodeEnd":76,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/desktop-client/src/components/mobile/budget/IncomeCategoryList.tsx#L40-L76","documentation":"IncomeCategoryList's drag-and-drop reorder throws when the dragged category ID (`e.keys[0]`) is not found in its `categories` array, mirroring the expense list invariant. It guarantees the category exists and has a group before reordering.","triggerScenarios":"`onReorder` fires with a key absent from `categories` — the income category was deleted, the list was replaced between drag start and drop, or the drag key came from another list.","commonSituations":"Category deleted via sync mid-drag; dragging between expense and income lists; stale component state after budget data reload.","solutions":["Refresh the income categories list and retry the reorder.","Confirm the drag source keys are bound to this list's categories.","Return early instead of throwing when the lookup misses.","Check for concurrent category deletion during sync."],"exampleFix":"// before\nif (!categoryToMove) {\n  throw new Error(`Internal error: category with ID ${categoryIdToMove} not found.`);\n}\n// after\nif (!categoryToMove) {\n  console.warn(`Skipping reorder: category ${categoryIdToMove} no longer exists`);\n  return;\n}","handlingStrategy":"validation","validationCode":"const [key] = e.keys;\nconst categoryToMove = categories.find(c => c.id === key);\nif (!categoryToMove) return; // stale key; skip reorder","typeGuard":"function categoryExists(categories: { id: string }[], id: string): boolean {\n  return categories.some(c => c.id === id);\n}","tryCatchPattern":"try {\n  onReorder(e);\n} catch (err) {\n  if (String(err).includes('not found')) {\n    refreshCategories();\n  } else {\n    throw err;\n  }\n}","preventionTips":["Validate drag keys against the live income categories list.","Refresh list state after deletions or sync updates.","Keep drag sources and reorder handlers bound to the same data.","Treat missed lookups as no-ops in UI reorder flows."],"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"}