{"record":{"id":"4b86521eb40be619","repo":"actualbudget/actual","slug":"internal-error-category-with-id-targetcategoryi-4b8652","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/IncomeCategoryList.tsx","lineNumber":83,"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":65,"sourceCodeEnd":101,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/desktop-client/src/components/mobile/budget/IncomeCategoryList.tsx#L65-L101","documentation":"IncomeCategoryList throws this while handling a drag-and-drop category reorder: after a drop event it searches the local `categories` array for the drop target's ID and throws when `findIndex` returns -1. It is an invariant assertion meaning the UI's drag payload referenced a category that is no longer present in the rendered category list. Actual Budget throws it to fail fast rather than silently corrupt the category ordering.","triggerScenarios":"A drag-and-drop `onDrop`-style handler fires with `e.target.dropPosition === 'after'` and the drop target's `targetCategoryId` does not exist in the `categories` array in state — e.g. the category was deleted or hidden by another action/device while the drag was in flight, or a stale drag payload references an old category ID.","commonSituations":"Users dragging a category onto another category that a concurrent sync or undo just removed; a stale mobile UI after category deletion on another device; a bug where the drag source passes a groupId or transient ID instead of a real category id.","solutions":["Refresh the category list / reload the budget so the local `categories` array matches the server, then retry the drag.","Check that the drag handler passes the target category's real `id` (not a group id or display index).","Guard the reorder: if `targetCategoryIndex === -1`, log and return instead of rethrowing in production.","If reproducible, file a bug with the category IDs involved — it indicates state desync between the drag layer and category state."],"exampleFix":"// before\nconst targetCategoryIndex = categories.findIndex(c => c.id === targetCategoryId);\nif (targetCategoryIndex === -1) {\n  throw new Error(`Internal error: category with ID ${targetCategoryId} not found.`);\n}\n// after\nconst targetCategoryIndex = categories.findIndex(c => c.id === targetCategoryId);\nif (targetCategoryIndex === -1) {\n  console.warn('Drop target category no longer exists:', targetCategoryId);\n  return; // gracefully ignore stale drag\n}","handlingStrategy":"validation","validationCode":"// before initiating/consuming a drag on a category\nconst exists = categories.some(c => c.id === targetCategoryId);\nif (!exists) {\n  refreshCategories(); // reload state before allowing the drop\n  return;\n}","typeGuard":"function isKnownCategory(id: string, categories: { id: string }[]): boolean {\n  return categories.some(c => c.id === id);\n}","tryCatchPattern":"try {\n  onDrop(e);\n} catch (err) {\n  if (String(err).includes('not found')) {\n    refreshCategories();\n  } else {\n    throw err;\n  }\n}","preventionTips":["Re-fetch categories after any delete/undo operation before allowing further drag-and-drop.","Never cache category IDs across budget reloads; resolve IDs at interaction time.","Clear active drag state when the underlying list changes."],"tags":["react","drag-and-drop","state-desync","invariant"],"backgroundTag":"entity-not-found-in-collection","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}