actualbudget/actual · error

Internal error: category group with ID ${groupIdToMove} not

Error message

Internal error: category group with ID ${groupIdToMove} not found.

What it means

ExpenseGroupList's onReorder handler throws when the dragged group ID (`e.keys[0]`) cannot be found in `categoryGroups`. The invariant ensures the group being moved actually exists before computing its insertion position.

Source

Thrown at packages/desktop-client/src/components/mobile/budget/ExpenseGroupList.tsx:97

        <ExpenseGroupHeader
          categoryGroup={group}
          month={month}
          showBudgetedColumn={showBudgetedColumn}
          show3Columns={show3Columns}
          onEditCategoryGroup={() => {}}
          isCollapsed={() => true}
          onToggleCollapse={() => {}}
          isHidden={false}
        />
      );
    },
    onReorder: e => {
      const [key] = e.keys;
      const groupIdToMove = key as CategoryGroupEntity['id'];
      const groupToMove = categoryGroups.find(c => c.id === groupIdToMove);

      if (!groupToMove) {
        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(

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Refresh categoryGroups from the store and retry the reorder.
  2. Bind the drag source keys to the same array passed to onReorder.
  3. Return early (no-op) when the group lookup misses instead of throwing.
  4. Audit for concurrent group deletions during active drags.

Example fix

// before
if (!groupToMove) {
  throw new Error(`Internal error: category group with ID ${groupIdToMove} not found.`);
}
// after
if (!groupToMove) {
  console.warn(`Skipping reorder: group ${groupIdToMove} no longer exists`);
  return;
}
Defensive patterns

Strategy: validation

Validate before calling

const [key] = e.keys;
const groupToMove = categoryGroups.find(c => c.id === key);
if (!groupToMove) return; // stale key; skip reorder

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

When it happens

Trigger: `onReorder` fires with a key whose `categoryGroups.find(c => c.id === groupIdToMove)` returns undefined — group deleted concurrently, list filtered, or drag payload from a stale render.

Common situations: Group removed via sync/another tab mid-drag; dragging a group header whose list was re-rendered with fresh IDs; drag started before a budget-month switch replaced the groups.

Related errors


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/3e3d0ff6e1c26f52. Report an issue: GitHub.