actualbudget/actual · error

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

Error message

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

What it means

ExpenseCategoryList throws when computing an 'after'-position drop and the target category ID is not present in the `categories` array. The invariant guarantees `categories[targetCategoryIndex + 1]` is meaningful before inserting the moved category.

Source

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

          `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)

Solutions

  1. Refresh categories from the store and retry the drag.
  2. Verify the drop target belongs to the same category list handling onReorder.
  3. No-op (return) when the target index is -1 instead of crashing the reorder.
  4. Investigate concurrent deletions via sync if this recurs.

Example fix

// before
if (targetCategoryIndex === -1) {
  throw new Error(`Internal error: category with ID ${targetCategoryId} not found.`);
}
// after
if (targetCategoryIndex === -1) {
  console.warn(`Drop target ${targetCategoryId} no longer exists; ignoring`);
  return;
}
Defensive patterns

Strategy: validation

Validate before calling

const targetCategoryIndex = categories.findIndex(c => c.id === targetCategoryId);
if (e.target.dropPosition === 'after' && targetCategoryIndex === -1) {
  return; // stale target; skip
}

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: dropPosition === 'after' with `categories.findIndex(c => c.id === targetCategoryId)` returning -1: the drop target key is stale, from another list, or the category was removed concurrently.

Common situations: Target category deleted on another device mid-drag; drop target rendered by a different (group) list; list re-render replaced category IDs between drag start and drop.

Related errors


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