actualbudget/actual · error

An '${existingGroup.name}' account group already exists.

Error message

An '${existingGroup.name}' account group already exists.

What it means

insertAccountGroup rejects creating an account group whose name case-insensitively matches any existing non-deleted (tombstone = 0) group. It looks up UPPER(name) in account_groups and throws if found. The thrown message includes the existing group's stored name.

Source

Thrown at packages/loot-core/src/server/db/index.ts:802

  });
}

export function getAccountGroups() {
  return all<DbAccountGroup>(
    `SELECT * FROM account_groups WHERE tombstone = 0 ORDER BY sort_order, id`,
  );
}

export async function insertAccountGroup(
  group: WithRequired<Partial<DbAccountGroup>, 'name'>,
): Promise<DbAccountGroup['id']> {
  // Don't allow duplicate group
  const existingGroup = await first<Pick<DbAccountGroup, 'id' | 'name'>>(
    `SELECT id, name FROM account_groups WHERE UPPER(name) = ? AND tombstone = 0 LIMIT 1`,
    [group.name.toUpperCase()],
  );
  if (existingGroup) {
    throw new Error(`An '${existingGroup.name}' account group already exists.`);
  }

  const lastGroup = await first<Pick<DbAccountGroup, 'sort_order'>>(`
    SELECT sort_order FROM account_groups WHERE tombstone = 0 ORDER BY sort_order DESC, id DESC LIMIT 1
  `);
  const sort_order = (lastGroup ? lastGroup.sort_order : 0) + SORT_INCREMENT;

  group = {
    ...accountGroupModel.validate(group),
    sort_order,
  };
  const id: DbAccountGroup['id'] = await insertWithUUID(
    'account_groups',
    group,
  );
  return id;
}

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Use a different, unique group name
  2. Query account_groups first for a case-insensitive match and reuse/update the existing group instead
  3. If the previous group was deleted, note tombstoned groups are excluded so their name can be reused — the error means a live group holds the name
  4. Catch the error and show the conflicting group name in the UI

Example fix

// before
await aqlQuery.insertAccountGroup({ name: 'Savings' });
// after
const groups = await aqlQuery.getAccountGroups();
if (!groups.some(g => g.name.toLowerCase() === 'savings')) {
  await aqlQuery.insertAccountGroup({ name: 'Savings' });
}
Defensive patterns

Strategy: validation

Validate before calling

const groups = await aqlQuery.getAccountGroups();
if (groups.some(g => g.name.toUpperCase() === newName.toUpperCase())) {
  throw new Error(`Account group '${newName}' already exists`);
}

Type guard

function isNameAvailable(groups: { name: string }[], name: string): boolean {
  return !groups.some(g => g.name.toUpperCase() === name.toUpperCase());
}

Try / catch

try {
  await aqlQuery.insertAccountGroup(group);
} catch (e) {
  if (e.message.includes('account group already exists')) {
    notifyUser(`A group named '${group.name}' already exists`);
  } else throw e;
}

Prevention

When it happens

Trigger: Calling insertAccountGroup with a name equal (ignoring case) to an existing live group, e.g. 'Checking' when 'CHECKING' exists; duplicate submits of the same create form; imports that carry over group names already present in the budget.

Common situations: Users creating account groups manually with a reused name, migrating budgets or importing CSV/QIF that recreates existing groups, retry logic re-issuing a create after a timeout.

Related errors


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