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

  1. Refresh categoryGroups and retry the drag operation.
  2. Ensure drop targets only render keys of groups present in categoryGroups.
  3. No-op when the target index is -1 instead of throwing.
  4. 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

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


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