actualbudget/actual · error

Internal error: category with ID

Error message

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

What it means

IncomeCategoryList throws this while handling a drag-and-drop category reorder: after a drop event it searches the local `categories` array for the drop target's ID and throws when `findIndex` returns -1. It is an invariant assertion meaning the UI's drag payload referenced a category that is no longer present in the rendered category list. Actual Budget throws it to fail fast rather than silently corrupt the category ordering.

Solutions

  1. Refresh the category list / reload the budget so the local `categories` array matches the server, then retry the drag.
  2. Check that the drag handler passes the target category's real `id` (not a group id or display index).
  3. Guard the reorder: if `targetCategoryIndex === -1`, log and return instead of rethrowing in production.
  4. If reproducible, file a bug with the category IDs involved — it indicates state desync between the drag layer and category state.

Example fix

// before
const targetCategoryIndex = categories.findIndex(c => c.id === targetCategoryId);
if (targetCategoryIndex === -1) {
  throw new Error(`Internal error: category with ID ${targetCategoryId} not found.`);
}
// after
const targetCategoryIndex = categories.findIndex(c => c.id === targetCategoryId);
if (targetCategoryIndex === -1) {
  console.warn('Drop target category no longer exists:', targetCategoryId);
  return; // gracefully ignore stale drag
}
Defensive patterns

Strategy: validation

Validate before calling

// before initiating/consuming a drag on a category
const exists = categories.some(c => c.id === targetCategoryId);
if (!exists) {
  refreshCategories(); // reload state before allowing the drop
  return;
}

Type guard

function isKnownCategory(id: string, categories: { id: string }[]): boolean {
  return categories.some(c => c.id === id);
}

Try / catch

try {
  onDrop(e);
} catch (err) {
  if (String(err).includes('not found')) {
    refreshCategories();
  } else {
    throw err;
  }
}

Prevention

When it happens

Trigger: A drag-and-drop `onDrop`-style handler fires with `e.target.dropPosition === 'after'` and the drop target's `targetCategoryId` does not exist in the `categories` array in state — e.g. the category was deleted or hidden by another action/device while the drag was in flight, or a stale drag payload references an old category ID.

Common situations: Users dragging a category onto another category that a concurrent sync or undo just removed; a stale mobile UI after category deletion on another device; a bug where the drag source passes a groupId or transient ID instead of a real category id.

Related errors


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

Appendix: source

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

          `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) {
          throw new Error(
            `Internal error: category with ID ${targetCategoryId} not found.`,
          );
        }

        const nextToTargetCategory = categories[targetCategoryIndex + 1];

        moveCategory.mutate({
          id: categoryToMove.id,
          groupId: categoryToMove.group,
          // 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)