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
- Refresh categories from the store and retry the drag.
- Verify the drop target belongs to the same category list handling onReorder.
- No-op (return) when the target index is -1 instead of crashing the reorder.
- 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
- Re-check drop target existence immediately before index math.
- Only render drop targets whose keys exist in the categories array.
- Refresh list state on sync events that delete categories.
- Log and ignore stale drops rather than crashing the handler.
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
- Internal error: account with ID ${targetAccountId} not found
- Internal error: category with ID ${categoryIdToMove} not fou
- Internal error: category group with ID ${groupIdToMove} not
- Internal error: category group with ID ${targetGroupId} not
- Internal error: category with ID ${categoryIdToMove} not fou
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/36328773f619e76d.
Report an issue: GitHub.