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

  1. Pick a different, unique name for the new category group.
  2. If the existing group is the one you want, unhide/reuse it instead of creating a new one.
  3. 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

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


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