actualbudget/actual · error

Internal error: category with ID ${categoryIdToMove} not fou

Error message

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

What it means

IncomeCategoryList's drag-and-drop reorder throws when the dragged category ID (`e.keys[0]`) is not found in its `categories` array, mirroring the expense list invariant. It guarantees the category exists and has a group before reordering.

Source

Thrown at packages/desktop-client/src/components/mobile/budget/IncomeCategoryList.tsx:58

          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

  1. Refresh the income categories list and retry the reorder.
  2. Confirm the drag source keys are bound to this list's categories.
  3. Return early instead of throwing when the lookup misses.
  4. 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

When it happens

Trigger: `onReorder` fires with a key absent from `categories` — the income category was deleted, the list was replaced between drag start and drop, or the drag key came from another list.

Common situations: Category deleted via sync mid-drag; dragging between expense and income lists; stale component state after budget data reload.

Related errors


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