actualbudget/actual · error

A tag with that name already exists

Error message

A tag with that name already exists

What it means

renameTag() enforces tag-name uniqueness: if any other tag (different id) already uses the target name, it throws Error('A tag with that name already exists'). Renaming a tag to its current name is a no-op that returns the id early.

Source

Thrown at packages/loot-core/src/server/tags/app.ts:135

  tag: newTag,
}: Pick<TagEntity, 'id' | 'tag'>): Promise<TagEntity['id']> {
  const name = newTag.trim();
  // accept any char except whitespaces and '#', same as tag creation
  if (!/^[^#\s]+$/.test(name)) {
    throw new Error('Invalid tag name');
  }

  const tags = await db.getTags();
  const allTags = await db.getAllTags();
  const existing = tags.find(t => t.id === id);
  if (!existing) {
    throw new Error('Tag not found');
  }
  if (existing.tag === name) {
    return id;
  }
  if (allTags.some(t => t.id !== id && t.tag === name)) {
    throw new Error('A tag with that name already exists');
  }

  await batchMessages(async () => {
    await db.updateTag({ id, tag: name });

    for (const { id: transactionId, notes } of await db.findTags()) {
      const renamed = renameTagInNotes(notes, existing.tag, name);
      if (renamed !== notes) {
        await db.updateTransaction({ id: transactionId, notes: renamed });
      }
    }
  });

  return id;
}

async function discoverTags(): Promise<TagEntity[]> {
  const taggedNotes = await db.findTags();

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Check existing tag names and reject/normalize duplicates in the UI before calling renameTag
  2. If the intent is a merge, use merge/collapse functionality or manually re-point notes instead of renaming onto an existing name
  3. Catch the error and surface 'name already in use' to the user

Example fix

// before
await renameTag({ id, tag: 'groceries' }); // Error: already exists
// after
const all = await getAllTags();
if (all.some(t => t.id !== id && t.tag === 'groceries')) {
  throw new Error('A tag with that name already exists');
}
await renameTag({ id, tag: 'groceries' });
Defensive patterns

Strategy: validation

Validate before calling

const all = await getAllTags();
if (all.some(t => t.id !== id && t.tag === name.trim())) {
  throw new Error('A tag with that name already exists');
}

Type guard

function isDuplicateTagName(e: unknown): boolean {
  return e instanceof Error &&
    e.message === 'A tag with that name already exists';
}

Try / catch

try {
  await renameTag({ id, tag });
} catch (e) {
  if (isDuplicateTagName(e)) {
    notifyUser('That tag name is already in use');
  } else throw e;
}

Prevention

When it happens

Trigger: renameTag({ id, tag }) targets a name present in db.getAllTags() under a different id — e.g. renaming 'food' to 'groceries' when 'groceries' already exists.

Common situations: Merging two similar tags by renaming one onto the other; users typing a name that already exists; automated scripts iterating renames that collide.

Related errors


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