actualbudget/actual · error

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

Error message

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

What it means

ExpenseGroupList's renderDragPreview looks up the dragged group ID from the drag payload's 'text/plain' data and throws if no matching category group exists in `categoryGroups`. This guarantees the preview can render a real ExpenseGroupHeader for the dragged item.

Source

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

      return (
        <DropIndicator
          target={target}
          className={css({
            '&[data-drop-target]': {
              height: 4,
              backgroundColor: theme.tableBorderSeparator,
              opacity: 1,
              borderRadius: 4,
            },
          })}
        />
      );
    },
    renderDragPreview: items => {
      const draggedGroupId = items[0]['text/plain'];
      const group = categoryGroups.find(c => c.id === draggedGroupId);
      if (!group) {
        throw new Error(
          `Internal error: category group with ID ${draggedGroupId} not found.`,
        );
      }
      return (
        <ExpenseGroupHeader
          categoryGroup={group}
          month={month}
          showBudgetedColumn={showBudgetedColumn}
          show3Columns={show3Columns}
          onEditCategoryGroup={() => {}}
          isCollapsed={() => true}
          onToggleCollapse={() => {}}
          isHidden={false}
        />
      );
    },
    onReorder: e => {
      const [key] = e.keys;

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Ensure the drag payload's 'text/plain' value is exactly the category group ID.
  2. Refresh categoryGroups so the dragged group is present before/while dragging.
  3. Render a fallback preview (or return null) instead of throwing when the group is missing.
  4. Check for concurrent group deletion via sync.

Example fix

// before
if (!group) {
  throw new Error(`Internal error: category group with ID ${draggedGroupId} not found.`);
}
// after
if (!group) {
  return null; // group removed mid-drag; skip preview
}
Defensive patterns

Strategy: fallback

Validate before calling

const draggedGroupId = items[0]['text/plain'];
const group = categoryGroups.find(c => c.id === draggedGroupId);
if (!group) return null; // render no preview

Type guard

function groupExists(groups: { id: string }[], id: string): boolean {
  return groups.some(g => g.id === id);
}

Try / catch

try {
  return renderDragPreview(items);
} catch (err) {
  if (String(err).includes('not found')) {
    return null; // fallback: no custom preview
  }
  throw err;
}

Prevention

When it happens

Trigger: The drag payload's `items[0]['text/plain']` contains an ID absent from `categoryGroups` — the payload ID format changed, the group was deleted mid-drag, or the preview is rendered after the list was refreshed without that group.

Common situations: Group deleted on another device while dragging; a refactor changed what is serialized into 'text/plain' so it no longer equals a group ID; preview rendering racing a list reload.

Related errors


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