actualbudget/actual · error
A ${existingGroup.hidden ? 'hidden ' : ''}'${existingGroup.n
Error message
A ${existingGroup.hidden ? 'hidden ' : ''}'${existingGroup.name}' category group already exists. What it means
insertCategoryGroup performs a case-insensitive uniqueness check (UPPER(name) = ? AND tombstone = 0) before inserting. If any live category group already has the same name (case-insensitive), it throws a message that also indicates whether the existing group is hidden.
Source
Thrown at packages/loot-core/src/server/db/index.ts:370
return groups.map(group => ({
...group,
categories: categories.filter(c => c.cat_group === group.id),
}));
}
export async function insertCategoryGroup(
group: WithRequired<Partial<DbCategoryGroup>, 'name'>,
): Promise<DbCategoryGroup['id']> {
// Don't allow duplicate group
const existingGroup = await first<
Pick<DbCategoryGroup, 'id' | 'name' | 'hidden'>
>(
`SELECT id, name, hidden FROM category_groups WHERE UPPER(name) = ? and tombstone = 0 LIMIT 1`,
[group.name.toUpperCase()],
);
if (existingGroup) {
throw new Error(
`A ${
existingGroup.hidden ? 'hidden ' : ''
}'${existingGroup.name}' category group already exists.`,
);
}
const lastGroup = await first<Pick<DbCategoryGroup, 'sort_order'>>(`
SELECT sort_order FROM category_groups WHERE tombstone = 0 ORDER BY sort_order DESC, id DESC LIMIT 1
`);
const sort_order = (lastGroup ? lastGroup.sort_order : 0) + SORT_INCREMENT;
group = {
...categoryGroupModel.validate(group),
sort_order,
};
const id: DbCategoryGroup['id'] = await insertWithUUID(
'category_groups',
group,View on GitHub (pinned to d4334cb6e6)
Solutions
- Pick a different, unique name for the new category group.
- If the existing group is the one you want, unhide/reuse it instead of creating a new one.
- If the duplicate is deleted, note tombstoned groups are ignored — the clash is with a live (tombstone = 0) group; rename or restore it.
Example fix
// before
await send('create-category-group', { name: 'Bills' }); // 'bills' exists
// after
const groups = await getCategories();
const exists = groups.group.some(g => g.name.toLowerCase() === 'bills');
if (!exists) await send('create-category-group', { name: 'Bills' }); Defensive patterns
Strategy: validation
Validate before calling
const groups = (await send('get-categories')).group;
if (groups.some(g => g.name.toLowerCase() === name.toLowerCase())) {
throw new Error(`A category group named '${name}' already exists (possibly hidden).`);
} Try / catch
try {
await send('create-category-group', { name });
} catch (e) {
if (/category group already exists/.test(e.message)) {
console.error(`Name conflict: ${e.message} Reuse or rename the existing group.`);
}
} Prevention
- Check existing groups (including hidden ones) case-insensitively before creating.
- Make setup/import scripts idempotent — skip creation when the group exists.
- Remember hidden groups still reserve their names.
When it happens
Trigger: Calling 'create-category-group' / insertCategoryGroup with a name matching an existing group case-insensitively, e.g. 'Bills' when 'bills' exists — including when the existing group is hidden.
Common situations: Users recreating a group they previously only hid (hidden groups still count); importers/setup scripts re-running and re-adding default groups like 'Bills' or 'Income'; renaming a group to collide with another.
Related errors
- Category '${category.name}' already exists in group '${categ
- No update fields provided. Use --name or --hidden.
- A category with the name "${category.name}" already exists.
- Only one `up to` allowed per category
- Only one spend template is allowed per category
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/4dd9f1ceac751b55.
Report an issue: GitHub.