actualbudget/actual · error

A category with the name "${category.name}" already exists.

Error message

A category with the name "${category.name}" already exists.

What it means

updateCategory translates a SQLite UNIQUE constraint violation on the categories.name column into this user-facing error, since category names must be unique within the budget.

Source

Thrown at packages/loot-core/src/server/budget/app.ts:345

    is_income: isIncome ? 1 : 0,
    hidden: hidden ? 1 : 0,
  });
}

async function updateCategory(category: CategoryEntity): Promise<void> {
  try {
    await db.updateCategory(
      categoryModel.toDb({
        ...category,
        name: category.name.trim(),
      }),
    );
  } catch (e) {
    if (
      e instanceof Error &&
      e.message.toLowerCase().includes('unique constraint')
    ) {
      throw new Error(
        `A category with the name "${category.name}" already exists.`,
        { cause: e },
      );
    }
    throw e;
  }
}

async function moveCategory({
  id,
  groupId,
  targetId,
}: {
  id: CategoryEntity['id'];
  groupId: CategoryGroupEntity['id'];
  targetId: CategoryEntity['id'] | null;
}): Promise<void> {
  await batchMessages(async () => {

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Check for an existing category with the target name (case-insensitively) before renaming.
  2. Catch this error in the UI and prompt the user to pick a different name.
  3. If duplicates are intentional, merge/delete the existing category first.

Example fix

// before
await updateCategory({ id, name: 'Groceries' }); // throws if 'Groceries' exists
// after
const existing = await q('categories').filter({ name: 'Groceries' }).select('*');
if (existing.length === 0) {
  await updateCategory({ id, name: 'Groceries' });
}
Defensive patterns

Strategy: try-catch

Validate before calling

const dup = await q('categories').select('name').execute();
const exists = dup.some(c => c.name.toLowerCase() === newName.toLowerCase());
if (exists) throw new Error(`A category named "${newName}" already exists`);

Try / catch

try {
  await app.updateCategory({ id, name: newName });
} catch (e) {
  if (e.message.includes('already exists')) {
    showBanner(`Rename failed: a category named "${newName}" exists`);
  } else throw e;
}

Prevention

When it happens

Trigger: Renaming a category (updateCategory) to a name that already exists in the same category group.

Common situations: Importing categories from CSV/YNAB where duplicates exist; UI allowing rename without a prior duplicate check; case variations that collide after SQLite's case-insensitive uniqueness rules.

Related errors


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