{"record":{"id":"36328773f619e76d","repo":"actualbudget/actual","slug":"internal-error-category-with-id-targetcategoryi","errorCode":null,"errorMessage":"Internal error: category with ID ${targetCategoryId} 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/ExpenseCategoryList.tsx","lineNumber":94,"sourceCode":"          `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        });\n      } else if (e.target.dropPosition === 'after') {\n        const targetCategoryIndex = categories.findIndex(\n          c => c.id === targetCategoryId,\n        );\n\n        if (targetCategoryIndex === -1) {\n          throw new Error(\n            `Internal error: category with ID ${targetCategoryId} not found.`,\n          );\n        }\n\n        const nextToTargetCategory = categories[targetCategoryIndex + 1];\n\n        moveCategory.mutate({\n          id: categoryToMove.id,\n          groupId: categoryToMove.group,\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    },","sourceCodeStart":76,"sourceCodeEnd":112,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/desktop-client/src/components/mobile/budget/ExpenseCategoryList.tsx#L76-L112","documentation":"ExpenseCategoryList throws when computing an 'after'-position drop and the target category ID is not present in the `categories` array. The invariant guarantees `categories[targetCategoryIndex + 1]` is meaningful before inserting the moved category.","triggerScenarios":"dropPosition === 'after' with `categories.findIndex(c => c.id === targetCategoryId)` returning -1: the drop target key is stale, from another list, or the category was removed concurrently.","commonSituations":"Target category deleted on another device mid-drag; drop target rendered by a different (group) list; list re-render replaced category IDs between drag start and drop.","solutions":["Refresh categories from the store and retry the drag.","Verify the drop target belongs to the same category list handling onReorder.","No-op (return) when the target index is -1 instead of crashing the reorder.","Investigate concurrent deletions via sync if this recurs."],"exampleFix":"// before\nif (targetCategoryIndex === -1) {\n  throw new Error(`Internal error: category with ID ${targetCategoryId} not found.`);\n}\n// after\nif (targetCategoryIndex === -1) {\n  console.warn(`Drop target ${targetCategoryId} no longer exists; ignoring`);\n  return;\n}","handlingStrategy":"validation","validationCode":"const targetCategoryIndex = categories.findIndex(c => c.id === targetCategoryId);\nif (e.target.dropPosition === 'after' && targetCategoryIndex === -1) {\n  return; // stale target; skip\n}","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":["Re-check drop target existence immediately before index math.","Only render drop targets whose keys exist in the categories array.","Refresh list state on sync events that delete categories.","Log and ignore stale drops rather than crashing the handler."],"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"}