{"record":{"id":"e8a09e6abb3052e3","repo":"actualbudget/actual","slug":"category-category-name-already-exists-in-grou","errorCode":null,"errorMessage":"Category '${category.name}' already exists in group '${category.cat_group}'","messagePattern":"Category '(.+?)' already exists in group '(.+?)'","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/loot-core/src/server/db/index.ts","lineNumber":456,"sourceCode":"  await Promise.all(categories.map(cat => deleteCategory(cat, transferId)));\n  await delete_('category_groups', group.id);\n}\n\nexport async function insertCategory(\n  category: WithRequired<Partial<DbCategory>, 'name' | 'cat_group'>,\n  { atEnd }: { atEnd?: boolean | undefined } = { atEnd: undefined },\n): Promise<DbCategory['id']> {\n  let sort_order;\n\n  let id_: DbCategory['id'];\n  await batchMessages(async () => {\n    // Dont allow duplicated names in groups\n    const existingCatInGroup = await first<Pick<DbCategory, 'id'>>(\n      `SELECT id FROM categories WHERE cat_group = ? and UPPER(name) = ? and tombstone = 0 LIMIT 1`,\n      [category.cat_group, category.name.toUpperCase()],\n    );\n    if (existingCatInGroup) {\n      throw new Error(\n        `Category '${category.name}' already exists in group '${category.cat_group}'`,\n      );\n    }\n\n    if (atEnd) {\n      const lastCat = await first<Pick<DbCategory, 'sort_order'>>(`\n        SELECT sort_order FROM categories WHERE tombstone = 0 ORDER BY sort_order DESC, id DESC LIMIT 1\n      `);\n      sort_order = (lastCat ? lastCat.sort_order : 0) + SORT_INCREMENT;\n    } else {\n      // Unfortunately since we insert at the beginning, we need to shove\n      // the sort orders to make sure there's room for it\n      const categories = await all<Pick<DbCategory, 'id' | 'sort_order'>>(\n        `SELECT id, sort_order FROM categories WHERE cat_group = ? AND tombstone = 0 ORDER BY sort_order, id`,\n        [category.cat_group],\n      );\n\n      const { updates, sort_order: order } = shoveSortOrders(","sourceCodeStart":438,"sourceCodeEnd":474,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/loot-core/src/server/db/index.ts#L438-L474","documentation":"insertCategory enforces unique non-deleted category names within a category group (case-insensitive). Before inserting, it queries categories with the same cat_group and UPPER(name) where tombstone = 0; if one exists, it throws. The group is referenced by id, so the message shows an id, not a friendly group name.","triggerScenarios":"Calling insertCategory with a name that duplicates (ignoring case) an existing non-deleted category in the same group; also occurs when a deleted-but-tombstoned category is excluded so a name that 'should' be free is not, or when the client retries a create that already succeeded.","commonSituations":"UI autocomplete not filtering duplicates, batch imports of categories with repeated names in one group, creating 'Groceries' when 'groceries' already exists in the group, concurrent clients syncing the same new category.","solutions":["Rename the new category or pick a different group before calling insertCategory","Check for an existing category first: SELECT id FROM categories WHERE cat_group = ? AND UPPER(name) = ? AND tombstone = 0","If the old category was deleted (tombstoned), restore/reuse it instead of creating a new one","Handle the error in the client and surface a 'name already in use' message to the user"],"exampleFix":"// before\nawait aqlQuery.insertCategory({ name: 'Groceries', cat_group: groupId });\n// after\nconst existing = await runQuery(q('categories').filter({ cat_group: groupId, name: 'Groceries' }).select('id'));\nif (existing.length === 0) {\n  await aqlQuery.insertCategory({ name: 'Groceries', cat_group: groupId });\n}","handlingStrategy":"validation","validationCode":"const dup = await runQuery(q('categories').filter({ cat_group: groupId }).select('name'));\nconst isDuplicate = dup.some(c => c.name.toLowerCase() === newName.toLowerCase());\nif (isDuplicate) throw new Error(`Category '${newName}' already exists in this group`);","typeGuard":"function isUniqueName<T extends { name: string }>(list: T[], name: string): boolean {\n  return !list.some(item => item.name.toUpperCase() === name.toUpperCase());\n}","tryCatchPattern":"try {\n  await aqlQuery.insertCategory(category);\n} catch (e) {\n  if (e.message.includes('already exists in group')) {\n    notifyUser('Category name already used in this group');\n  } else throw e;\n}","preventionTips":["Check for case-insensitive name duplicates within the group before creating","Filter out tombstoned categories in your duplicate check so deleted names are reusable","Disable duplicate submit buttons to avoid double-create retries","Surface live name validation in the UI as the user types"],"tags":["validation","duplicate","categories","database"],"backgroundTag":"duplicate-name-conflict","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}