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 by TagService.create when a tag with the same value already exists for the authenticated user. Tag values are unique per user, and child tags compose the full path (parentValue/name), so duplicates collide on the composed value. It is a 400 BadRequest signaling a uniqueness constraint violation.

Solutions

  1. Check for the existing tag first (GET /api/tags) and reuse it instead of creating
  2. Use upsert semantics: fetch by value; if found, return/update it, otherwise create
  3. Add client-side deduplication/normalization (trim, collapse slashes) before sending
  4. If a retry, treat this 400 as success and look up the previously created tag

Example fix

// before
const tag = await api.createTag({ name: 'travel' });
// after
const existing = tags.find(t => t.value === 'travel');
const tag = existing ?? await api.createTag({ name: 'travel' });
Defensive patterns

Strategy: validation

Validate before calling

const tags = await api.getAllTags();
if (tags.some(t => t.value === name)) {
  return tags.find(t => t.value === name); // reuse instead of creating
}

Try / catch

try { return await api.createTag({ name }); } catch (e) {
  if (e.response?.status === 400 && /already exists/.test(e.response?.data?.message ?? '')) {
    return (await api.getAllTags()).find(t => t.value === name);
  }
  throw e;
}

Prevention

When it happens

Trigger: POST /api/tags with a name whose composed value (parent.value + '/' + name, or just name for root tags) matches an existing tag of the same user — including case-sensitivity quirks, hidden characters, or whitespace differences that still collide.

Common situations: Retrying a request after a timeout when the first call actually succeeded; importing tags from another instance; nested-tag name collisions where different parents' children share names but the composed path duplicates; users renaming parents so paths collide.

Understand the failure class

Background: "already exists" / EEXIST / FileAlreadyExistsException: what the 'file already exists' error means and how to fix it — this error's family across 37 libraries.

Related errors


AI-assisted analysis of immich-app/immich@e55ac299a4 (2026-09-15). Data as JSON: /api/errors/cd420f5490106102. Report an issue: GitHub.

Appendix: 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 { name, color } = dto;
    const existing = await this.findOrFail(id);

    let value;
    if (name) {
      const parts = existing.value.split('/');
      parts[parts.length - 1] = name;

View on GitHub (pinned to e55ac299a4)