immich-app/immich · error · BadRequestException

A tag with that name already exists

Error message

A tag with that name already exists

What it means

Thrown (as BadRequestException) by TagService.create when tagRepository.getByValue(userId, value) returns an existing tag. The composite value (parent path + name) must be unique per user, so any collision - including case-only differences routed through the same value - is rejected.

Source

Thrown at server/src/services/tag.service.ts:50

    const tag = await this.findOrFail(id);
    return mapTag(tag);
  }

  async create(auth: AuthDto, dto: TagCreateDto) {
    let parent;
    if (dto.parentId) {
      await this.requireAccess({ auth, permission: Permission.TagRead, ids: [dto.parentId] });
      parent = await this.tagRepository.get(dto.parentId);
      if (!parent) {
        throw new BadRequestException('Tag not found');
      }
    }

    const userId = auth.user.id;
    const value = parent ? `${parent.value}/${dto.name}` : dto.name;
    const duplicate = await this.tagRepository.getByValue(userId, value);
    if (duplicate) {
      throw new BadRequestException(`A tag with that name already exists`);
    }

    const { color } = dto;
    const tag = await this.tagRepository.create({ userId, value, color, parentId: parent?.id });

    return mapTag(tag);
  }

  async update(auth: AuthDto, id: string, dto: TagUpdateDto): Promise<TagResponseDto> {
    await this.requireAccess({ auth, permission: Permission.TagUpdate, ids: [id] });

    const { color } = dto;
    const tag = await this.tagRepository.update(id, { color });
    return mapTag(tag);
  }

  async upsert(auth: AuthDto, dto: TagUpsertDto) {
    const tags = await upsertTags(this.tagRepository, { userId: auth.user.id, tags: dto.tags });

View on GitHub (pinned to 199723261c)

Solutions

  1. Reuse the existing tag instead of creating a new one (GET /tags and search by value).
  2. Choose a distinct name or parent so the composite value differs.
  3. If the existing tag is soft-deleted, hard-delete or restore it first.

Example fix

// before
await tagsApi.create({ name: 'cats' }); // exists already
// after
const existing = await tagsApi.getAll().then(t => t.find(x => x.value === 'cats'));
if (existing) return existing;
await tagsApi.create({ name: 'cats' });
Defensive patterns

Strategy: validation

Validate before calling

const value = parent ? `${parent.value}/${dto.name}` : dto.name;
const dup = await tagRepository.getByValue(userId, value);
if (dup) { /* offer to reuse dup instead of POSTing */ }

Try / catch

try { await tagsApi.create(dto); }
catch (e) {
  if (e instanceof BadRequestException && /already exists/.test(e.message)) {
    // switch to update-or-reuse flow for the existing tag
  }
}

Prevention

When it happens

Trigger: POST /tags with a name that, combined with the optional parent, reproduces an existing tag value for that user.

Common situations: User re-creates a tag they just deleted but soft-delete still holds the value; renaming attempt that collides; parent path produces an unintended duplicate.

Related errors


AI-assisted analysis of immich-app/immich@199723261c (2026-08-12). Data as JSON: /api/errors/cd420f5490106102. Report an issue: GitHub.