actualbudget/actual · error
Internal error: category group with ID ${targetGroupId} not
Error message
Internal error: category group with ID ${targetGroupId} not found. What it means
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.
Source
Thrown at packages/desktop-client/src/components/mobile/budget/ExpenseGroupList.tsx:115
throw new Error(
`Internal error: category group with ID ${groupIdToMove} not found.`,
);
}
const targetGroupId = e.target.key as CategoryEntity['id'];
if (e.target.dropPosition === 'before') {
moveCategoryGroup.mutate({
id: groupToMove.id,
targetId: targetGroupId,
});
} else if (e.target.dropPosition === 'after') {
const targetGroupIndex = categoryGroups.findIndex(
c => c.id === targetGroupId,
);
if (targetGroupIndex === -1) {
throw new Error(
`Internal error: category group with ID ${targetGroupId} not found.`,
);
}
const nextToTargetCategory = categoryGroups[targetGroupIndex + 1];
moveCategoryGroup.mutate({
id: groupToMove.id,
// Due to the way `moveCategory` works, we use the category next to the
// actual target category here because `moveCategory` always shoves the
// category *before* the target category.
// On the other hand, using `null` as `targetId` moves the category
// to the end of the list.
targetId: nextToTargetCategory?.id || null,
});
}
},
});View on GitHub (pinned to d4334cb6e6)
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.
Example fix
// before
if (targetGroupIndex === -1) {
throw new Error(`Internal error: category group with ID ${targetGroupId} not found.`);
}
// after
if (targetGroupIndex === -1) {
console.warn(`Drop target ${targetGroupId} no longer exists; ignoring`);
return;
} Defensive patterns
Strategy: validation
Validate before calling
const targetGroupIndex = categoryGroups.findIndex(c => c.id === targetGroupId);
if (e.target.dropPosition === 'after' && targetGroupIndex === -1) {
return; // stale target; skip
} Type guard
function groupExists(groups: { id: string }[], id: string): boolean {
return groups.some(g => g.id === id);
} Try / catch
try {
onReorder(e);
} catch (err) {
if (String(err).includes('not found')) {
refreshCategoryGroups();
} else {
throw err;
}
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Internal error: account with ID ${targetAccountId} not found
- Internal error: category with ID ${categoryIdToMove} not fou
- Internal error: category with ID ${targetCategoryId} not fou
- Internal error: category group with ID ${groupIdToMove} not
- Internal error: category with ID ${categoryIdToMove} not fou
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/8c13d06980115406.
Report an issue: GitHub.