actualbudget/actual · error
Internal error: category with ID ${categoryIdToMove} not fou
Error message
Internal error: category with ID ${categoryIdToMove} not found. What it means
ExpenseCategoryList's drag-and-drop reorder throws when the dragged category ID (`e.keys[0]`) is not found in the local `categories` array. This invariant guarantees `categoryToMove` is valid before computing its new position. It indicates the drag payload references a category that no longer exists in the rendered list.
Source
Thrown at packages/desktop-client/src/components/mobile/budget/ExpenseCategoryList.tsx:69
target={target}
className={css({
'&[data-drop-target]': {
height: 4,
backgroundColor: theme.tableBorderSeparator,
opacity: 1,
borderRadius: 4,
},
})}
/>
);
},
onReorder: e => {
const [key] = e.keys;
const categoryIdToMove = key as CategoryEntity['id'];
const categoryToMove = categories.find(c => c.id === categoryIdToMove);
if (!categoryToMove) {
throw new Error(
`Internal error: category with ID ${categoryIdToMove} not found.`,
);
}
if (!categoryToMove.group) {
throw new Error(
`Internal error: category ${categoryIdToMove} is not in a group and cannot be moved.`,
);
}
const targetCategoryId = e.target.key as CategoryEntity['id'];
if (e.target.dropPosition === 'before') {
moveCategory.mutate({
id: categoryToMove.id,
groupId: categoryToMove.group,
targetId: targetCategoryId,
});View on GitHub (pinned to d4334cb6e6)
Solutions
- Refresh the categories list from the store and retry the reorder.
- Confirm the drag source is bound to the same list that handles onReorder (keys must come from the rendered categories).
- Return early instead of throwing when the lookup misses, treating it as a no-op stale drop.
- Check for concurrent category deletion during sync.
Example fix
// before
if (!categoryToMove) {
throw new Error(`Internal error: category with ID ${categoryIdToMove} not found.`);
}
// after
if (!categoryToMove) {
console.warn(`Skipping reorder: category ${categoryIdToMove} no longer exists`);
return;
} Defensive patterns
Strategy: validation
Validate before calling
const [key] = e.keys; const categoryToMove = categories.find(c => c.id === key); if (!categoryToMove) return; // stale key; skip reorder
Type guard
function categoryExists(categories: { id: string }[], id: string): boolean {
return categories.some(c => c.id === id);
} Try / catch
try {
onReorder(e);
} catch (err) {
if (String(err).includes('not found')) {
refreshCategories();
} else {
throw err;
}
} Prevention
- Validate drag keys against the live list before reorder logic.
- Keep drag source and reorder handler bound to the same array.
- Refresh list state after any delete/sync that could affect categories.
- Prefer warn-and-return over throw for stale UI interactions.
When it happens
Trigger: `onReorder` fires with a key whose category lookup (`categories.find(c => c.id === categoryIdToMove)`) returns undefined — the dragged category was deleted, filtered out, or the list was replaced between drag start and drop.
Common situations: Category deleted via sync or another tab mid-drag; category belongs to a different group's list but was dragged; stale component state after category recreation with a new ID.
Related errors
- Internal error: account with ID ${targetAccountId} not found
- Internal error: category with ID ${targetCategoryId} not fou
- Internal error: category group with ID ${groupIdToMove} not
- Internal error: category group with ID ${targetGroupId} not
- Internal error: category with ID ${categoryIdToMove} not fou
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/2d558d995ce67740.
Report an issue: GitHub.