actualbudget/actual · error

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

Error message

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

What it means

ExpenseCategoryList's drag-and-drop reorder throws when the dragged category ID (`e.keys[0]`) is not found in the local `categories` array. This invariant guarantees `categoryToMove` is valid before computing its new position. It indicates the drag payload references a category that no longer exists in the rendered list.

Source

Thrown at packages/desktop-client/src/components/mobile/budget/ExpenseCategoryList.tsx:69

          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 categories list from the store and retry the reorder.
  2. Confirm the drag source is bound to the same list that handles onReorder (keys must come from the rendered categories).
  3. Return early instead of throwing when the lookup misses, treating it as a no-op stale drop.
  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 whose category lookup (`categories.find(c => c.id === categoryIdToMove)`) returns undefined — the dragged category was deleted, filtered out, or the list was replaced between drag start and drop.

Common situations: Category deleted via sync or another tab mid-drag; category belongs to a different group's list but was dragged; stale component state after category recreation with a new ID.

Related errors


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