{"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 { name, color } = dto;\n    const existing = await this.findOrFail(id);\n\n    let value;\n    if (name) {\n      const parts = existing.value.split('/');\n      parts[parts.length - 1] = name;","sourceCodeStart":32,"sourceCodeEnd":68,"githubUrl":"https://github.com/immich-app/immich/blob/e55ac299a4ec7cb372e35dbf2c6c05ee9ce77f6c/server/src/services/tag.service.ts#L32-L68","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Check for the existing tag first (GET /api/tags) and reuse it instead of creating","Use upsert semantics: fetch by value; if found, return/update it, otherwise create","Add client-side deduplication/normalization (trim, collapse slashes) before sending","If a retry, treat this 400 as success and look up the previously created tag"],"exampleFix":"// before\nconst tag = await api.createTag({ name: 'travel' });\n// after\nconst existing = tags.find(t => t.value === 'travel');\nconst tag = existing ?? await api.createTag({ name: 'travel' });","handlingStrategy":"validation","validationCode":"const tags = await api.getAllTags();\nif (tags.some(t => t.value === name)) {\n  return tags.find(t => t.value === name); // reuse instead of creating\n}","typeGuard":null,"tryCatchPattern":"try { return await api.createTag({ name }); } catch (e) {\n  if (e.response?.status === 400 && /already exists/.test(e.response?.data?.message ?? '')) {\n    return (await api.getAllTags()).find(t => t.value === name);\n  }\n  throw e;\n}","preventionTips":["Deduplicate names client-side before creation","Normalize input (trim whitespace, collapse slashes)","Make create idempotent: look up by value first","Serialize create requests to avoid double submits"],"tags":["duplicate","uniqueness"],"backgroundTag":"file-already-exists","analyzedSha":"e55ac299a4ec7cb372e35dbf2c6c05ee9ce77f6c","analyzedAt":"2026-09-15T07:20:19.675Z","contentChangedAt":"2026-09-15T07:20:19.675Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}