{"record":{"id":"cd420f5490106102","repo":"immich-app/immich","slug":"a-tag-with-that-name-already-exists","errorCode":null,"errorMessage":"A tag with that name already exists","messagePattern":"A tag with that name already exists","errorType":"exception","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"server/src/services/tag.service.ts","lineNumber":50,"sourceCode":"    const tag = await this.findOrFail(id);\n    return mapTag(tag);\n  }\n\n  async create(auth: AuthDto, dto: TagCreateDto) {\n    let parent;\n    if (dto.parentId) {\n      await this.requireAccess({ auth, permission: Permission.TagRead, ids: [dto.parentId] });\n      parent = await this.tagRepository.get(dto.parentId);\n      if (!parent) {\n        throw new BadRequestException('Tag not found');\n      }\n    }\n\n    const userId = auth.user.id;\n    const value = parent ? `${parent.value}/${dto.name}` : dto.name;\n    const duplicate = await this.tagRepository.getByValue(userId, value);\n    if (duplicate) {\n      throw new BadRequestException(`A tag with that name already exists`);\n    }\n\n    const { color } = dto;\n    const tag = await this.tagRepository.create({ userId, value, color, parentId: parent?.id });\n\n    return mapTag(tag);\n  }\n\n  async update(auth: AuthDto, id: string, dto: TagUpdateDto): Promise<TagResponseDto> {\n    await this.requireAccess({ auth, permission: Permission.TagUpdate, ids: [id] });\n\n    const { color } = dto;\n    const tag = await this.tagRepository.update(id, { color });\n    return mapTag(tag);\n  }\n\n  async upsert(auth: AuthDto, dto: TagUpsertDto) {\n    const tags = await upsertTags(this.tagRepository, { userId: auth.user.id, tags: dto.tags });","sourceCodeStart":32,"sourceCodeEnd":68,"githubUrl":"https://github.com/immich-app/immich/blob/199723261c6ffa897fec8ccdaea6359e39c37cc3/server/src/services/tag.service.ts#L32-L68","documentation":"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.","triggerScenarios":"POST /tags with a name that, combined with the optional parent, reproduces an existing tag value for that user.","commonSituations":"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.","solutions":["Reuse the existing tag instead of creating a new one (GET /tags and search by value).","Choose a distinct name or parent so the composite value differs.","If the existing tag is soft-deleted, hard-delete or restore it first."],"exampleFix":"// before\nawait tagsApi.create({ name: 'cats' }); // exists already\n// after\nconst existing = await tagsApi.getAll().then(t => t.find(x => x.value === 'cats'));\nif (existing) return existing;\nawait tagsApi.create({ name: 'cats' });","handlingStrategy":"validation","validationCode":"const value = parent ? `${parent.value}/${dto.name}` : dto.name;\nconst dup = await tagRepository.getByValue(userId, value);\nif (dup) { /* offer to reuse dup instead of POSTing */ }","typeGuard":null,"tryCatchPattern":"try { await tagsApi.create(dto); }\ncatch (e) {\n  if (e instanceof BadRequestException && /already exists/.test(e.message)) {\n    // switch to update-or-reuse flow for the existing tag\n  }\n}","preventionTips":["Search existing tags by value before offering create.","Compute the composite value client-side to preview collisions.","Hard-delete or restore soft-deleted tags before reusing their values."],"tags":["tags","validation","duplicate","bad-request"],"backgroundTag":null,"analyzedSha":"199723261c6ffa897fec8ccdaea6359e39c37cc3","analyzedAt":"2026-08-12T04:54:27.085Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}