actualbudget/actual · error

Internal error: category ${categoryIdToMove} is not in a gro

Error message

Internal error: category ${categoryIdToMove} is not in a group and cannot be moved.

What it means

ExpenseCategoryList throws this when the dragged category exists but has no `group` reference, which is required to reorder it relative to other categories. The component treats a group-less category as an invalid internal state that cannot be safely moved.

Source

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

              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,
        });
      } else if (e.target.dropPosition === 'after') {
        const targetCategoryIndex = categories.findIndex(
          c => c.id === targetCategoryId,
        );

        if (targetCategoryIndex === -1) {

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Ensure every category in the list is loaded with its group relation (check the query/select feeding `categories`).
  2. Assign the orphan category to a group before attempting reordering.
  3. Validate category data integrity after import/sync operations.
  4. Skip group-less categories during reorder instead of throwing.

Example fix

// before
if (!categoryToMove.group) {
  throw new Error(`Internal error: category ${categoryIdToMove} is not in a group and cannot be moved.`);
}
// after
if (!categoryToMove.group) {
  console.warn(`Category ${categoryIdToMove} has no group; skipping reorder`);
  return;
}
Defensive patterns

Strategy: validation

Validate before calling

if (!categoryToMove?.group) {
  console.warn(`Category ${categoryIdToMove} has no group; skipping reorder`);
  return;
}

Type guard

function hasGroup(c: CategoryEntity): c is CategoryEntity & { group: NonNullable<CategoryEntity['group']> } {
  return Boolean(c.group);
}

Try / catch

try {
  onReorder(e);
} catch (err) {
  if (String(err).includes('not in a group')) {
    refreshCategories();
  } else {
    throw err;
  }
}

Prevention

When it happens

Trigger: `onReorder` is called with a category whose `categoryToMove.group` is undefined/null — e.g. the category was loaded without its group relation or belongs to no group in the data model.

Common situations: Data imported or synced without group assignments; a query that selects categories without joining/including their group; a category created programmatically without a group ID.

Related errors


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